EdgeAIG's picture
download
raw
22.3 kB
/**
* Generates OpenAPI 3.1 documents from declarative `HttpApi` contracts.
*
* The generator reads API groups, endpoints, schemas, security definitions, and
* annotations, then produces an OpenAPI document. This module also provides the
* annotations used to shape that output and the TypeScript model for the
* OpenAPI objects it generates.
*
* @since 4.0.0
*/
import * as Arr from "../../Array.js";
import * as Context from "../../Context.js";
import { constFalse } from "../../Function.js";
import * as JsonPatch from "../../JsonPatch.js";
import { escapeToken } from "../../JsonPointer.js";
import * as JsonSchema from "../../JsonSchema.js";
import * as Option from "../../Option.js";
import * as Schema from "../../Schema.js";
import * as SchemaAST from "../../SchemaAST.js";
import * as SchemaRepresentation from "../../SchemaRepresentation.js";
import * as HttpMethod from "../http/HttpMethod.js";
import * as HttpApi from "./HttpApi.js";
import * as HttpApiEndpoint from "./HttpApiEndpoint.js";
import * as HttpApiMiddleware from "./HttpApiMiddleware.js";
import * as HttpApiSchema from "./HttpApiSchema.js";
/**
* OpenAPI annotation for overriding generated identifiers, including operation ids.
*
* @category annotations
* @since 4.0.0
*/
export class Identifier extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Identifier") {}
/**
* OpenAPI annotation for setting the API title or group tag name.
*
* @category annotations
* @since 4.0.0
*/
export class Title extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Title") {}
/**
* OpenAPI annotation for setting the generated API version.
*
* @category annotations
* @since 4.0.0
*/
export class Version extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Version") {}
/**
* OpenAPI annotation for setting generated descriptions on APIs, groups, endpoints, or security schemes.
*
* @category annotations
* @since 4.0.0
*/
export class Description extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Description") {}
/**
* OpenAPI annotation for setting the generated API license metadata.
*
* @category annotations
* @since 4.0.0
*/
export class License extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/License") {}
/**
* OpenAPI annotation for adding external documentation metadata to groups or endpoints.
*
* @category annotations
* @since 4.0.0
*/
export class ExternalDocs extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/ExternalDocs") {}
/**
* OpenAPI annotation for setting the generated API server list.
*
* @category annotations
* @since 4.0.0
*/
export class Servers extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Servers") {}
/**
* OpenAPI annotation for setting the format metadata, such as a bearer token format on security schemes.
*
* @category annotations
* @since 4.0.0
*/
export class Format extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Format") {}
/**
* OpenAPI annotation for setting generated summary text.
*
* @category annotations
* @since 4.0.0
*/
export class Summary extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Summary") {}
/**
* OpenAPI annotation for marking a generated endpoint operation as deprecated.
*
* @category annotations
* @since 4.0.0
*/
export class Deprecated extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Deprecated") {}
/**
* OpenAPI annotation for shallowly merging additional fields into a generated OpenAPI object.
*
* @category annotations
* @since 4.0.0
*/
export class Override extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Override") {}
/**
* Annotation that excludes an annotated group or endpoint from the generated
* OpenAPI specification.
*
* **When to use**
*
* Use to hide internal, experimental, or otherwise undocumented HTTP API groups
* and endpoints from generated OpenAPI output.
*
* @category annotations
* @since 4.0.0
*/
export const Exclude = /*#__PURE__*/Context.Reference("effect/httpapi/OpenApi/Exclude", {
defaultValue: constFalse
});
/**
* OpenAPI annotation for transforming a generated OpenAPI object.
*
* **Details**
*
* The function is applied during generation to the annotated API, group tag, or
* endpoint operation.
*
* @category annotations
* @since 4.0.0
*/
export class Transform extends /*#__PURE__*/Context.Service()("effect/httpapi/OpenApi/Transform") {}
const servicesPartial = tags => {
const entries = Object.entries(tags);
return options => {
let context = Context.empty();
for (const [key, tag] of entries) {
if (options[key] !== undefined) {
context = Context.add(context, tag, options[key]);
}
}
return context;
};
};
/**
* Builds a `Context` containing OpenAPI annotations from the supplied options.
*
* @category annotations
* @since 4.0.0
*/
export const annotations = /*#__PURE__*/servicesPartial({
identifier: Identifier,
title: Title,
version: Version,
description: Description,
license: License,
summary: Summary,
deprecated: Deprecated,
externalDocs: ExternalDocs,
servers: Servers,
format: Format,
override: Override,
exclude: Exclude,
transform: Transform
});
const apiCache = /*#__PURE__*/new WeakMap();
/**
* This function checks if a given tag exists within the provided context. If
* the tag is present, it retrieves the associated value and applies the given
* callback function to it. If the tag is not found, the function does nothing.
*/
function processAnnotation(ctx, annotation, f) {
const o = Context.getOption(ctx, annotation);
if (Option.isSome(o)) {
f(o.value);
}
}
/**
* Converts an `HttpApi` instance into an OpenAPI Specification object.
*
* **Details**
*
* This function takes an `HttpApi` instance, which defines a structured API,
* and generates an OpenAPI Specification (`OpenAPISpec`). The resulting spec
* adheres to the OpenAPI 3.1.0 standard and includes detailed metadata such as
* paths, operations, security schemes, and components. The function processes
* the API's annotations, middleware, groups, and endpoints to build a complete
* and accurate representation of the API in OpenAPI format.
*
* The function also deduplicates schemas, applies transformations, and
* integrates annotations like descriptions, summaries, external documentation,
* and overrides. Cached results are used for better performance when the same
* `HttpApi` instance is processed multiple times.
*
* @category constructors
* @since 4.0.0
*/
export function fromApi(api) {
const cached = apiCache.get(api);
if (cached !== undefined) {
return cached;
}
let spec = {
openapi: "3.1.0",
info: {
title: "Api",
version: "0.0.1"
},
paths: {},
components: {
schemas: {},
securitySchemes: {}
},
security: [],
tags: []
};
const pathOps = [];
processAnnotation(api.annotations, Title, title => {
spec.info.title = title;
});
processAnnotation(api.annotations, Version, version => {
spec.info.version = version;
});
processAnnotation(api.annotations, Description, description => {
spec.info.description = description;
});
processAnnotation(api.annotations, License, license => {
spec.info.license = license;
});
processAnnotation(api.annotations, Summary, summary => {
spec.info.summary = summary;
});
processAnnotation(api.annotations, Servers, servers => {
spec.servers = [...servers];
});
HttpApi.reflect(api, {
onGroup({
group
}) {
if (Context.get(group.annotations, Exclude)) {
return;
}
let tag = {
name: Context.getOrElse(group.annotations, Title, () => group.identifier)
};
processAnnotation(group.annotations, Description, description => {
tag.description = description;
});
processAnnotation(group.annotations, ExternalDocs, externalDocs => {
tag.externalDocs = externalDocs;
});
processAnnotation(group.annotations, Override, override => {
Object.assign(tag, override);
});
processAnnotation(group.annotations, Transform, transformFn => {
tag = transformFn(tag);
});
spec.tags.push(tag);
},
onEndpoint({
endpoint,
group,
mergedAnnotations,
middleware
}) {
if (Context.get(mergedAnnotations, Exclude)) {
return;
}
let op = {
tags: [Context.getOrElse(group.annotations, Title, () => group.identifier)],
operationId: Context.getOrElse(endpoint.annotations, Identifier, () => group.topLevel ? endpoint.name : `${group.identifier}.${endpoint.name}`),
parameters: [],
security: [],
responses: {}
};
const path = endpoint.path.replace(/:(\w+)\??/g, "{$1}");
const method = endpoint.method.toLowerCase();
function processRequestBodies(payloadMap) {
if (payloadMap.size > 0) {
const c = {};
let hasContent = false;
payloadMap.forEach(({
encoding,
schemas
}, contentType) => {
const filtered = schemas.filter(s => !HttpApiSchema.isNoContent(s.ast));
if (filtered.length === 0) return;
hasContent = true;
const asts = filtered.map(SchemaAST.getAST);
const ast = asts.length === 1 ? asts[0] : new SchemaAST.Union(asts, "anyOf");
pathOps.push({
_tag: "schema",
ast: toEncodingAST(ast, encoding._tag),
path: ["paths", path, method, "requestBody", "content", contentType, "schema"]
});
c[contentType] = {
schema: {}
};
});
if (hasContent) {
op.requestBody = {
content: c,
required: true
};
}
}
}
function processResponseBodies(bodies, defaultDescription) {
for (const [status, {
content,
descriptions,
streamContent
}] of bodies) {
const description = descriptions.size > 0 ? Array.from(descriptions).join(" | ") : defaultDescription();
op.responses[status] = {
description
};
if (content !== undefined) {
content.forEach((map, encoding) => {
map.forEach((schemas, contentType) => {
const asts = Array.from(schemas, SchemaAST.getAST);
const ast = asts.length === 1 ? asts[0] : new SchemaAST.Union(asts, "anyOf");
pathOps.push({
_tag: "schema",
ast: toEncodingAST(ast, encoding),
path: ["paths", path, method, "responses", String(status), "content", contentType, "schema"]
});
op.responses[status].content ??= {};
op.responses[status].content[contentType] = {
schema: {}
};
});
});
}
if (streamContent !== undefined) {
streamContent.forEach((stream, contentType) => {
op.responses[status].content ??= {};
if (HttpApiSchema.isStreamSse(stream)) {
pathOps.push({
_tag: "schema",
ast: SchemaAST.getAST(stream.events),
path: ["paths", path, method, "responses", String(status), "content", contentType, "schema"]
});
pathOps.push({
_tag: "schema",
ast: SchemaAST.getAST(Schema.toCodecJson(Schema.Cause(stream.error, Schema.Defect()))),
path: ["paths", path, method, "responses", String(status), "content", contentType, "x-effect-stream", "causeSchema"]
});
pathOps.push({
_tag: "schema",
ast: SchemaAST.getAST(stream.error),
path: ["paths", path, method, "responses", String(status), "content", contentType, "x-effect-stream", "errorSchema"]
});
op.responses[status].content[contentType] = {
schema: {},
"x-effect-stream": {
encoding: "sse",
causeSchema: {},
errorSchema: {},
failureEvent: reservedStreamFailureEvent
}
};
} else {
op.responses[status].content[contentType] = {
schema: {
type: "string",
format: "binary"
},
"x-effect-stream": {
encoding: "uint8array"
}
};
}
});
}
}
}
function processParameters(schema, i) {
if (schema) {
const ast = SchemaAST.getLastEncoding(schema.ast);
if (SchemaAST.isObjects(ast)) {
for (const ps of ast.propertySignatures) {
op.parameters.push({
name: String(ps.name),
in: i,
schema: {},
required: i === "path" || !SchemaAST.isOptional(ps.type)
});
pathOps.push({
_tag: "parameter",
ast: ps.type,
path: ["paths", path, method, "parameters", String(op.parameters.length - 1), "schema"]
});
}
}
}
}
processAnnotation(endpoint.annotations, Description, description => {
op.description = description;
});
processAnnotation(endpoint.annotations, Summary, summary => {
op.summary = summary;
});
processAnnotation(endpoint.annotations, Deprecated, deprecated => {
op.deprecated = deprecated;
});
processAnnotation(endpoint.annotations, ExternalDocs, externalDocs => {
op.externalDocs = externalDocs;
});
middleware.forEach(middleware => {
if (!HttpApiMiddleware.isSecurity(middleware)) {
return;
}
for (const [name, security] of Object.entries(middleware.security)) {
processHttpApiSecurity(name, security);
op.security.push({
[name]: []
});
}
});
function processHttpApiSecurity(name, security) {
if (spec.components.securitySchemes[name] !== undefined) {
return;
}
spec.components.securitySchemes[name] = makeSecurityScheme(security);
}
const hasBody = HttpMethod.hasBody(endpoint.method);
if (hasBody) {
processRequestBodies(endpoint.payload);
}
processParameters(endpoint.params, "path");
if (!hasBody && endpoint.payload.size === 1) {
const entry = endpoint.payload.values().next().value;
processParameters(entry.schemas[0], "query");
}
processParameters(endpoint.headers, "header");
processParameters(endpoint.query, "query");
processResponseBodies(extractSuccessResponseBodies(endpoint), () => "Success");
processResponseBodies(extractResponseBodies(HttpApiEndpoint.getErrorSchemas(endpoint), HttpApiSchema.getStatusError, resolveDescriptionOrIdentifier), () => "Error");
if (!spec.paths[path]) {
spec.paths[path] = {};
}
processAnnotation(endpoint.annotations, Override, override => {
Object.assign(op, override);
});
processAnnotation(endpoint.annotations, Transform, transformFn => {
op = transformFn(op);
});
spec.paths[path][method] = op;
}
});
processAnnotation(api.annotations, HttpApi.AdditionalSchemas, componentSchemas => {
componentSchemas.forEach(componentSchema => {
const identifier = SchemaAST.resolveIdentifier(componentSchema.ast);
if (identifier !== undefined) {
if (identifier in spec.components.schemas) {
throw new globalThis.Error(`Duplicate component schema identifier: ${identifier}`);
}
spec.components.schemas[identifier] = {};
pathOps.push({
_tag: "schema",
ast: componentSchema.ast,
path: ["components", "schemas", identifier]
});
}
});
});
function escapePath(path) {
return "/" + path.map(escapeToken).join("/");
}
if (Arr.isArrayNonEmpty(pathOps)) {
const multiDocument = SchemaRepresentation.fromASTs(Arr.map(pathOps, op => op.ast));
const jsonSchemaMultiDocument = JsonSchema.toMultiDocumentOpenApi3_1(SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument));
const patchOps = pathOps.map((op, i) => {
const oppath = escapePath(op.path);
const value = jsonSchemaMultiDocument.schemas[i];
return {
op: "replace",
path: oppath,
value: value
};
});
Object.entries(jsonSchemaMultiDocument.definitions).forEach(([name, definition]) => {
patchOps.push({
op: "add",
path: escapePath(["components", "schemas", name]),
value: definition
});
});
spec = JsonPatch.apply(patchOps, spec);
}
Object.keys(spec.components.schemas).forEach(key => {
if (!JsonSchema.VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(key)) {
throw new globalThis.Error(`Invalid component schema key: ${key}`);
}
});
processAnnotation(api.annotations, Override, override => {
Object.assign(spec, override);
});
processAnnotation(api.annotations, Transform, transformFn => {
spec = transformFn(spec);
});
apiCache.set(api, spec);
return spec;
}
const reservedStreamFailureEvent = "effect/httpapi/stream/failure";
function extractSuccessResponseBodies(endpoint) {
return extractResponseBodies(HttpApiEndpoint.getSuccessSchemas(endpoint), HttpApiSchema.getStatusSuccess, resolveDescriptionOrIdentifier);
}
function extractResponseBodies(schemas, getStatus, getDescription) {
const map = new Map();
schemas.forEach(process);
return map;
function process(schema) {
if (HttpApiSchema.isStreamSchema(schema)) {
addStreamContent(schema);
return;
}
const ast = schema.ast;
const status = getStatus(ast);
if (HttpApiSchema.isNoContent(ast)) {
addNoContent(status, getDescription(schema.ast) ?? "<No Content>");
} else {
addContent(schema, status, HttpApiSchema.getResponseEncoding(ast));
}
}
function addNoContent(status, description) {
const statusMap = map.get(status);
if (statusMap === undefined) {
map.set(status, {
descriptions: new Set([description]),
content: undefined,
streamContent: undefined
});
} else {
if (description !== undefined) {
statusMap.descriptions.add(description);
}
}
}
function addContent(schema, status, encoding) {
const description = getDescription(schema.ast);
const statusMap = map.get(status);
const {
_tag,
contentType
} = encoding;
if (statusMap === undefined) {
map.set(status, {
descriptions: new Set(description !== undefined ? [description] : []),
content: new Map([[_tag, new Map([[contentType, new Set([schema])]])]]),
streamContent: undefined
});
} else {
// concat descriptions
if (description !== undefined) {
statusMap.descriptions.add(description);
}
if (statusMap.content === undefined) {
statusMap.content = new Map([[_tag, new Map([[contentType, new Set([schema])]])]]);
} else {
const schemasByContentType = statusMap.content.get(_tag);
if (schemasByContentType === undefined) {
statusMap.content.set(_tag, new Map([[contentType, new Set([schema])]]));
} else {
const set = schemasByContentType.get(contentType);
if (set === undefined) {
schemasByContentType.set(contentType, new Set([schema]));
} else {
set.add(schema);
}
}
}
}
}
function addStreamContent(stream) {
const status = HttpApiSchema.getStatusStream(stream);
const statusMap = map.get(status);
if (statusMap === undefined) {
map.set(status, {
descriptions: new Set(),
content: undefined,
streamContent: new Map([[stream.contentType, stream]])
});
} else if (statusMap.streamContent === undefined) {
statusMap.streamContent = new Map([[stream.contentType, stream]]);
} else {
statusMap.streamContent.set(stream.contentType, stream);
}
}
}
function resolveDescriptionOrIdentifier(ast) {
return SchemaAST.resolveDescription(ast) ?? SchemaAST.resolveIdentifier(ast);
}
const Uint8ArrayEncoding = /*#__PURE__*/Schema.String.annotate({
format: "binary"
});
function toEncodingAST(ast, _tag) {
switch (_tag) {
case "Uint8Array":
return Uint8ArrayEncoding.ast;
case "Text":
return Schema.String.ast;
case "FormUrlEncoded":
case "Json":
return ast;
case "Multipart":
return persistedFileToBinaryEncoding(ast);
}
}
function persistedFileToBinaryEncoding(ast) {
if (SchemaAST.isDeclaration(ast) && ast.annotations?.typeConstructor?._tag === "effect/http/PersistedFile") {
return Uint8ArrayEncoding.ast;
}
if (typeof ast?.recur === "function") {
return ast.recur(persistedFileToBinaryEncoding);
}
return ast;
}
const makeSecurityScheme = security => {
const meta = {};
processAnnotation(security.annotations, Description, description => {
meta.description = description;
});
switch (security._tag) {
case "Basic":
{
return {
...meta,
type: "http",
scheme: "basic"
};
}
case "Http":
{
const format = Context.getOption(security.annotations, Format).pipe(Option.map(format => ({
bearerFormat: format
})), Option.getOrUndefined);
return {
...meta,
type: "http",
scheme: security.scheme,
...format
};
}
case "ApiKey":
{
return {
...meta,
type: "apiKey",
name: security.key,
in: security.in
};
}
}
};
//# sourceMappingURL=OpenApi.js.map

Xet Storage Details

Size:
22.3 kB
·
Xet hash:
beeba4f560f37f2c7c5053c768ca9ede670c1b74305e311f95b26592badbfec4

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.