diff --git a/plugins/plugin-effect-httpapiclient/index.md b/plugins/plugin-effect-httpapiclient/index.md new file mode 100644 index 00000000..4a267056 --- /dev/null +++ b/plugins/plugin-effect-httpapiclient/index.md @@ -0,0 +1,163 @@ +--- +layout: doc +title: Kubb Effect HttpApiClient Plugin +description: Generates Effect v4 HttpApi contracts and a typed HttpApiClient from your OpenAPI specification. +outline: deep +kind: plugin +id: plugin-effect-httpapiclient +name: Effect HttpApiClient +category: client +type: official +npmPackage: "@kubb/plugin-effect-httpapiclient" +repo: https://github.com/kubb-labs/plugins +docsPath: /plugins/plugin-effect-httpapiclient +maintainers: + - name: Stijn Van Hulle + github: stijnvanhulle +compatibility: + kubb: ">=5.0.0" + node: ">=22" +tags: + - effect + - api-client + - http-api + - security + - codegen + - openapi +dependencies: + - plugin-effect +resources: + documentation: https://kubb.dev/plugins/plugin-effect-httpapiclient + repository: https://github.com/kubb-labs/plugins + issues: https://github.com/kubb-labs/plugins/issues + changelog: https://github.com/kubb-labs/plugins/blob/main/packages/plugin-effect-httpapiclient/CHANGELOG.md + codesandbox: https://codesandbox.io/p/github/kubb-labs/plugins/main/examples/effect-httpapiclient +--- + +# @kubb/plugin-effect-httpapiclient + +`@kubb/plugin-effect-httpapiclient` generates Effect `HttpApiEndpoint`, `HttpApiGroup`, and `HttpApi` contracts from OpenAPI. It also exports a typed `ApiClient` Effect created by `HttpApiClient.make`. + +The plugin uses schemas generated by [`@kubb/plugin-effect`](/plugins/plugin-effect/), so include both plugins in your configuration. + +> [!WARNING] +> The first release targets `effect@4.0.0-beta.98`. Other Effect v4 beta releases and the future stable release may require changes to generated code. + +## Installation + +Install both Kubb plugins as development dependencies and Effect as an application dependency. + +::: code-group + +```shell [bun] +bun add -d @kubb/plugin-effect@beta @kubb/plugin-effect-httpapiclient@beta +bun add effect@4.0.0-beta.98 +``` + +```shell [pnpm] +pnpm add -D @kubb/plugin-effect@beta @kubb/plugin-effect-httpapiclient@beta +pnpm add effect@4.0.0-beta.98 +``` + +```shell [npm] +npm install --save-dev @kubb/plugin-effect@beta @kubb/plugin-effect-httpapiclient@beta +npm install effect@4.0.0-beta.98 +``` + +```shell [yarn] +yarn add -D @kubb/plugin-effect@beta @kubb/plugin-effect-httpapiclient@beta +yarn add effect@4.0.0-beta.98 +``` + +::: + +## Example + +Set `baseURL` to embed the service URL in the generated client. The default `mode: 'tag'` groups client methods by their first OpenAPI tag. + +```typescript twoslash [kubb.config.ts] +import { pluginEffect } from '@kubb/plugin-effect' +import { pluginEffectHttpApiClient } from '@kubb/plugin-effect-httpapiclient' +import { defineConfig } from 'kubb/config' + +export default defineConfig({ + input: './petStore.yaml', + output: { path: './src/gen' }, + plugins: [ + pluginEffect({ output: { path: 'effect' } }), + pluginEffectHttpApiClient({ + output: { path: 'effectHttpApiClient' }, + baseURL: 'https://petstore.example.com', + }), + ], +}) +``` + +Run the generated client as an Effect. Operations without a request body take `params`, `query`, and `headers` fields when their OpenAPI definition requires them. Operations with a body also take `payload`. + +```typescript [getPet.ts] +import { Effect } from 'effect' +import { ApiClient } from './gen/effectHttpApiClient' + +const getPet = Effect.gen(function* () { + const client = yield* ApiClient + return yield* client.pet.getPetById({ params: { petId: 10n } }) +}) +``` + +Set `mode: 'flat'` to expose operation methods directly on the client instead of grouping them by tag. + +## Authentication + +OpenAPI security requirements become required client middleware. The generated `makeSecurityLayer` accepts static credentials and an optional dynamic resolver. Static credentials take precedence, while the resolver supports refreshed tokens and OAuth scopes at request time. + +```typescript [authenticated.ts] +import { Effect, Redacted } from 'effect' +import { ApiClient, makeSecurityLayer } from './gen/effectHttpApiClient' + +const apiKey = process.env.PETSTORE_API_KEY +if (!apiKey) throw new Error('PETSTORE_API_KEY is required') + +const program = Effect.gen(function* () { + const client = yield* ApiClient + return yield* client.pet.getPetById({ params: { petId: 10n } }) +}).pipe( + Effect.provide( + makeSecurityLayer({ + credentials: { + api_key: { _tag: 'ApiKey', value: Redacted.make(apiKey) }, + }, + }), + ), +) +``` + +The middleware preserves OpenAPI security semantics: + +- Alternatives in the security array use OR semantics. The first complete alternative is applied. +- Schemes in one requirement object use AND semantics. +- Operation security replaces global security. An empty operation security array permits anonymous access. +- OAuth and OpenID Connect schemes receive a bearer credential. The plugin passes declared scopes to the dynamic resolver but does not acquire tokens. + +If no alternative can be satisfied, the request fails with `MissingSecurityCredentials` before it reaches the HTTP transport. A dynamic resolver failure becomes `SecurityCredentialResolutionError`. + +## Content and data types + +The generated contract uses Effect's native HttpApi schema support for JSON, URL-encoded forms, multipart forms, text, binary bodies, and server-sent events. XML and unrecognized text media types use a string fallback. Other unknown media types use `Uint8Array`. + +When the OpenAPI adapter uses `dateType: 'date'`, a `date-time` schema decodes to Effect's `DateTime.Utc` type and encodes to a UTC ISO string. + +Regular cookie parameters are exposed under `headers.cookies`. OpenAPI API keys stored in cookies are added by the generated security middleware. + +## Current constraints + +- `default` responses are omitted because Effect HttpApi contracts require concrete status codes. Generation fails for other nonnumeric response keys. +- The plugin generates client-side security middleware, not server-side authentication handlers. +- The generated `ApiClient` uses the `baseURL` configured during generation. It does not generate a client factory for changing the URL at runtime. + +## See also + +- [Options](/plugins/plugin-effect-httpapiclient/reference/options) +- [`@kubb/plugin-effect`](/plugins/plugin-effect/) +- [Effect HttpApi](https://effect.website/docs/http/http-api/) +- [Changelog](https://github.com/kubb-labs/plugins/blob/main/packages/plugin-effect-httpapiclient/CHANGELOG.md) diff --git a/plugins/plugin-effect-httpapiclient/reference/options.md b/plugins/plugin-effect-httpapiclient/reference/options.md new file mode 100644 index 00000000..27bab047 --- /dev/null +++ b/plugins/plugin-effect-httpapiclient/reference/options.md @@ -0,0 +1,138 @@ +--- +layout: doc +title: Options +description: Configuration options for @kubb/plugin-effect-httpapiclient. +outline: deep +--- + +# Options + +`pluginEffectHttpApiClient` accepts the following options. + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| [`output`](#output) | `Output` | `{ path: 'effectHttpApiClient', barrel: { type: 'named' } }` | Where the generated files are written and exported | +| [`group`](#group) | `Group` | None | Split generated files into per-tag or per-path folders | +| [`baseURL`](#baseurl) | `string` | None | Embed a fixed base URL in the generated client | +| [`mode`](#mode) | `'tag' \| 'flat'` | `'tag'` | Group client methods by tag or expose them at the root | +| [`include`](#include) | `Array` | None | Keep only operations that match | +| [`exclude`](#exclude) | `Array` | `[]` | Skip operations that match | +| [`override`](#override) | `Array` | `[]` | Apply different options per pattern | +| [`resolver`](#resolver) | `ResolverPatch` | None | Customize generated names and file paths | +| [`macros`](#macros) | `Array` | None | Rewrite AST nodes before printing | + +### output + +Controls where the generated `.ts` files are written and how Kubb exports them. + +#### output.path + +The plugin resolves this folder against the global `output.path` from `defineConfig`. + +| | | +| --------: | :-------------------- | +| Type: | `string` | +| Required: | `false` | +| Default: | `'effectHttpApiClient'` | + +#### output.barrel + + + +#### output.banner + + + +#### output.footer + + + +### group + + + +File grouping controls the output directory layout. It is separate from [`mode`](#mode), which controls the property layout on the generated `ApiClient`. + +#### group.type + + + +#### group.name + +The function turns a tag or path key into the subdirectory under `output.path`. + +| | | +| --------: | :--------------------------------------- | +| Type: | `(context: { group: string }) => string` | +| Required: | `false` | +| Default: | `({ group }) => camelCase(group)` | + +### baseURL + +Embeds a fixed string in the generated `HttpApiClient.make(Api, { baseUrl })` call. The plugin does not generate a factory for selecting another URL at runtime. + +| | | +| --------: | :-------- | +| Type: | `string` | +| Required: | `false` | +| Default: | None | + +### mode + +Controls the shape returned by the generated client Effect. + +- `'tag'` groups operations by their first OpenAPI tag. An operation without tags belongs to the `default` group. +- `'flat'` adds every operation directly to the client root. + +| | | +| --------: | :---------------- | +| Type: | `'tag' \| 'flat'` | +| Required: | `false` | +| Default: | `'tag'` | + +### include + + + +### exclude + + + +### override + + + +### resolver + +Changes generated symbols and file paths. Methods omitted from the patch keep the default behavior. + +| Method | Purpose | +| ------ | ------- | +| `name(name)` | Resolve the shared base name | +| `file(name)` | Resolve a generated file name | +| `endpoint.name(operation)` | Resolve an exported endpoint constant | +| `endpoint.identifier(operation)` | Resolve the operation property on the client | +| `group.name(tag)` | Resolve an exported `HttpApiGroup` constant | +| `group.identifier(tag)` | Resolve the group property on a tagged client | +| `api.name()` | Resolve the root `HttpApi` constant | +| `client.name()` | Resolve the generated client type and Effect value | + +```typescript [kubb.config.ts] +import { pluginEffectHttpApiClient } from '@kubb/plugin-effect-httpapiclient' + +pluginEffectHttpApiClient({ + resolver: { + client: { + name() { + return 'PetStoreClient' + }, + }, + }, +}) +``` + +See [Override a resolver](/docs/5.x/guide/going-further/resolvers) for the resolver context. + +### macros + + diff --git a/plugins/plugin-effect/index.md b/plugins/plugin-effect/index.md new file mode 100644 index 00000000..a84a7cd9 --- /dev/null +++ b/plugins/plugin-effect/index.md @@ -0,0 +1,117 @@ +--- +layout: doc +title: Kubb Effect Plugin +description: Generates Effect v4 schemas and matching TypeScript types from your OpenAPI specification. +outline: deep +kind: plugin +id: plugin-effect +name: Effect +category: validation +type: official +npmPackage: "@kubb/plugin-effect" +repo: https://github.com/kubb-labs/plugins +docsPath: /plugins/plugin-effect +maintainers: + - name: Stijn Van Hulle + github: stijnvanhulle +compatibility: + kubb: ">=5.0.0" + node: ">=22" +tags: + - effect + - validation + - schema + - runtime-validation + - codegen + - openapi +dependencies: [] +resources: + documentation: https://kubb.dev/plugins/plugin-effect + repository: https://github.com/kubb-labs/plugins + issues: https://github.com/kubb-labs/plugins/issues + changelog: https://github.com/kubb-labs/plugins/blob/main/packages/plugin-effect/CHANGELOG.md + codesandbox: https://codesandbox.io/p/github/kubb-labs/plugins/main/examples/effect +--- + +# @kubb/plugin-effect + +`@kubb/plugin-effect` generates [Effect](https://effect.website/) v4 schemas and matching TypeScript types from OpenAPI. Each schema uses the same PascalCase name in the type and value namespaces. + +```typescript [Pet.ts] +import * as Schema from 'effect/Schema' + +export type Pet = { + readonly id: number + readonly name: string +} + +export const Pet = Schema.Struct({ + id: Schema.Int, + name: Schema.String, +}) +``` + +> [!WARNING] +> The first release targets `effect@4.0.0-beta.98`. Other Effect v4 beta releases and the future stable release may require changes to generated code. + +## Installation + +Install the plugin as a development dependency and Effect as an application dependency. + +::: code-group + +```shell [bun] +bun add -d @kubb/plugin-effect@beta +bun add effect@4.0.0-beta.98 +``` + +```shell [pnpm] +pnpm add -D @kubb/plugin-effect@beta +pnpm add effect@4.0.0-beta.98 +``` + +```shell [npm] +npm install --save-dev @kubb/plugin-effect@beta +npm install effect@4.0.0-beta.98 +``` + +```shell [yarn] +yarn add -D @kubb/plugin-effect@beta +yarn add effect@4.0.0-beta.98 +``` + +::: + +## Example + +The plugin emits types itself, so it can replace `@kubb/plugin-ts` when Effect schemas are the source of truth. + +```typescript twoslash [kubb.config.ts] +import { pluginEffect } from '@kubb/plugin-effect' +import { defineConfig } from 'kubb' + +export default defineConfig({ + input: './petStore.yaml', + output: { path: './src/gen' }, + plugins: [ + pluginEffect({ + output: { path: './effect' }, + }), + ], +}) +``` + +> [!IMPORTANT] +> Do not write the default `pluginEffect()` and `pluginTs()` outputs into the same barrel. Both plugins export names such as `Pet`. Use separate output paths or add a suffix with `resolver` when one project needs both sets of types. + +## Runtime behavior + +OpenAPI length, range, pattern, uniqueness, and `oneOf` rules become Effect checks. OpenAPI `format` and `default` values become schema annotations. A format such as `email` does not reject a string, and a default does not fill a missing value. + +When the OpenAPI adapter uses `dateType: 'date'`, generated codecs decode `date-time` strings into `DateTime.Utc` values and encode them as UTC ISO strings. Date-only fields preserve their `YYYY-MM-DD` wire format, while the default string representation keeps both formats as annotated strings. JSON `int64` values decode from numbers to `bigint` values. + +## See also + +- [Effect Schema](https://effect.website/docs/schema/introduction/) +- [Options](/plugins/plugin-effect/reference/options) +- [Changelog](https://github.com/kubb-labs/plugins/blob/main/packages/plugin-effect/CHANGELOG.md) diff --git a/plugins/plugin-effect/reference/options.md b/plugins/plugin-effect/reference/options.md new file mode 100644 index 00000000..ad0ed6cf --- /dev/null +++ b/plugins/plugin-effect/reference/options.md @@ -0,0 +1,164 @@ +--- +layout: doc +title: Options +description: Configuration options for @kubb/plugin-effect. +outline: deep +--- + +# Options + +`pluginEffect` accepts the following options. + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| [`output`](#output) | `Output` | `{ path: 'effect', barrel: { type: 'named' } }` | Where the generated files are written and exported | +| [`group`](#group) | `Group` | None | Split output into per-tag or per-path folders | +| [`importPath`](#importpath) | `string` | `'effect/Schema'` | Module used for the generated Schema namespace import | +| [`regexType`](#regextype) | `'literal' \| 'constructor'` | `'constructor'` | How an OpenAPI `pattern` is written | +| [`include`](#include) | `Array` | None | Keep only operations that match | +| [`exclude`](#exclude) | `Array` | None | Skip operations that match | +| [`override`](#override) | `Array` | None | Apply different options per pattern | +| [`resolver`](#resolver) | `ResolverPatch` | None | Customize generated names and file paths | +| [`printer`](#printer) | `{ nodes?: PrinterEffectNodes }` | None | Replace the handler for a schema type | +| [`macros`](#macros) | `Array` | None | Rewrite AST nodes before printing | + +### output + +Controls where the generated `.ts` files are written and how Kubb exports them. + +#### output.path + +The plugin resolves this folder against the global `output.path` from `defineConfig`. For one generated file, set `output.mode: 'file'` and include a `.ts` extension. + +| | | +| -------: | :--------- | +| Type: | `string` | +| Required: | `false` | +| Default: | `'effect'` | + +#### output.mode + +`'directory'` writes one file per operation or component. `'file'` combines the generated schemas in the file named by `output.path`. + +| | | +| --------: | :---------------------- | +| Type: | `'directory' \| 'file'` | +| Required: | `false` | +| Default: | `'directory'` | + +#### output.barrel + + + +#### output.banner + + + +#### output.footer + + + +### group + + + +#### group.type + + + +#### group.name + +The function turns a tag or path key into the subdirectory under `output.path`. + +| | | +| --------: | :--------------------------------------- | +| Type: | `(context: { group: string }) => string` | +| Required: | `false` | +| Default: | `({ group }) => camelCase(group)` | + +### importPath + +Sets the module specifier for `import * as Schema from '...'` in generated files. A custom module must export the Effect Schema API as a namespace-compatible module. + +| | | +| --------: | :------------------ | +| Type: | `string` | +| Required: | `false` | +| Default: | `'effect/Schema'` | + +### regexType + +Controls the source emitted for OpenAPI `pattern` checks. + +- `'constructor'` emits `new RegExp('^[a-z]+$')` and is the default. +- `'literal'` emits `/^[a-z]+$/`. + +| | | +| --------: | :--------------------------- | +| Type: | `'literal' \| 'constructor'` | +| Required: | `false` | +| Default: | `'constructor'` | + +### include + + + +### exclude + + + +### override + + + +### resolver + +Changes generated symbols and file paths. Methods omitted from the patch keep the behavior from `resolverEffect`. + +The default resolver intentionally uses names such as `Pet` for both the schema and the type. Add a suffix if the same barrel also exports `plugin-ts` output. + +```typescript [kubb.config.ts] +import { pluginEffect } from '@kubb/plugin-effect' + +pluginEffect({ + resolver: { + name(name) { + return `${this.default.name(name)}Effect` + }, + }, +}) +``` + +See [Override a resolver](/docs/5.x/guide/going-further/resolvers) for the resolver context. + +### printer + +Replaces the handler for a schema node type. Every handler returns the runtime expression, decoded type, and encoded type so the three representations cannot drift apart. + +`this.base(node)` returns the built-in result. `this.transform(node)` prints a nested node. + +```typescript [kubb.config.ts] +import { pluginEffect } from '@kubb/plugin-effect' + +pluginEffect({ + printer: { + nodes: { + string(node) { + const base = this.base(node) + if (!base) return null + + return { + ...base, + runtime: `${base.runtime}.annotate({ title: 'Custom string' })`, + } + }, + }, + }, +}) +``` + +See the [printer guide](/docs/5.x/guide/going-further/printers) for the complete handler contract. + +### macros + +