feat(openapi): explicit object-component registration via createNamedObject (#1183)

Signed-off-by: Hampus Kraft <[email protected]>
This commit is contained in:
Hampus
2026-06-26 10:32:50 +02:00
committed by GitHub
parent 6e87f6e3d1
commit 6d537ef0db
6 changed files with 87 additions and 33 deletions
+33 -32
View File
@@ -31988,41 +31988,42 @@
},
"OAuth2AuthorizationsListResponse": {
"type": "array",
"items": {
"type": "object",
"properties": {
"application": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)$",
"description": "The unique identifier of the application"
},
"name": {"type": "string", "description": "The name of the application"},
"icon": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "The icon hash of the application"
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "The description of the application"
},
"bot_public": {"type": "boolean", "description": "Whether the bot can be invited by anyone"}
"items": {"$ref": "#/components/schemas/OAuth2AuthorizationResponse"}
},
"OAuth2AuthorizationResponse": {
"type": "object",
"properties": {
"application": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)$",
"description": "The unique identifier of the application"
},
"required": ["id", "name", "icon", "description", "bot_public"],
"description": "The application that was authorized"
"name": {"type": "string", "description": "The name of the application"},
"icon": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "The icon hash of the application"
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "The description of the application"
},
"bot_public": {"type": "boolean", "description": "Whether the bot can be invited by anyone"}
},
"scopes": {
"type": "array",
"items": {"type": "string"},
"maxItems": 50,
"description": "The list of granted OAuth2 scopes"
},
"authorized_at": {"type": "string", "description": "The timestamp when the authorization was granted"}
"required": ["id", "name", "icon", "description", "bot_public"],
"description": "The application that was authorized"
},
"required": ["application", "scopes", "authorized_at"]
}
"scopes": {
"type": "array",
"items": {"type": "string"},
"maxItems": 50,
"description": "The list of granted OAuth2 scopes"
},
"authorized_at": {"type": "string", "description": "The timestamp when the authorization was granted"}
},
"required": ["application", "scopes", "authorized_at"]
},
"OAuth2AuthorizationsBulkRevokeRequest": {
"type": "object",
@@ -119,6 +119,23 @@ export function getRegisteredDiscriminatedUnionBranchSchemas(): Record<string, O
return result;
}
const namedObjectRegistry = new Map<string, OpenAPISchema>();
export function getRegisteredNamedObjectSchemas(): Record<string, OpenAPISchema> {
const result: Record<string, OpenAPISchema> = {};
for (const [name, schema] of namedObjectRegistry) {
result[name] = schema;
}
return result;
}
function makeNamedObjectRef(name: string, fieldDescription: string | undefined): OpenAPISchemaOrRef {
if (fieldDescription) {
return {$ref: `#/components/schemas/${name}`, description: fieldDescription};
}
return {$ref: `#/components/schemas/${name}`};
}
function toPascalCase(value: string): string {
return value
.split(/[^A-Za-z0-9]+/)
@@ -437,6 +454,15 @@ export function zodToOpenAPISchema(schema: ZodTypeAny, depth = 0): OpenAPISchema
if (!shape) {
return {type: 'object'};
}
const objectAnnotation = parseFluxerTypeAnnotation(getDescription(schema));
const namedObjectName =
objectAnnotation?.typeName === 'NamedObject' ? objectAnnotation.objectName : undefined;
if (namedObjectName && depth > 0 && namedObjectRegistry.has(namedObjectName)) {
return makeNamedObjectRef(namedObjectName, objectAnnotation?.fieldDescription);
}
if (namedObjectName) {
namedObjectRegistry.set(namedObjectName, {type: 'object'});
}
const properties: Record<string, OpenAPISchemaOrRef> = {};
const required: Array<string> = [];
for (const [key, value] of Object.entries(shape)) {
@@ -452,6 +478,16 @@ export function zodToOpenAPISchema(schema: ZodTypeAny, depth = 0): OpenAPISchema
if (required.length > 0) {
result.required = required;
}
if (namedObjectName) {
if (objectAnnotation?.userDescription) {
result.description = objectAnnotation.userDescription;
}
namedObjectRegistry.set(namedObjectName, result);
if (depth > 0) {
return makeNamedObjectRef(namedObjectName, objectAnnotation?.fieldDescription);
}
return result;
}
return addDescription(result, schema);
}
case 'ZodOptional':
@@ -16,6 +16,7 @@ export interface FluxerTypeAnnotation {
enumEntries?: Array<EnumEntry>;
bitflagValues?: Array<BitflagEntry>;
bitflagTypeName?: string;
objectName?: string;
fieldDescription?: string;
}
interface BitflagEntryJson {
@@ -162,6 +163,10 @@ export function parseFluxerTypeAnnotation(description: string | undefined): Flux
userDescription = userDescription.slice(0, fieldDescIndex).trim() || undefined;
}
}
if (typeAndData.startsWith('NamedObject:')) {
const objectName = typeAndData.slice('NamedObject:'.length);
return {typeName: 'NamedObject', objectName, userDescription, fieldDescription};
}
if (typeAndData.startsWith('IntegerEnum:')) {
const namesStr = typeAndData.slice('IntegerEnum:'.length);
const enumNames = namesStr.split(',').filter(Boolean);
@@ -3,6 +3,7 @@ import {
getRegisteredBitflagSchemas,
getRegisteredDiscriminatedUnionBranchSchemas,
getRegisteredInt32EnumSchemas,
getRegisteredNamedObjectSchemas,
} from '@fluxer/openapi/src/converters/ZodToOpenAPI';
import {OpenAPIGeneratorCatalog} from '@fluxer/openapi/src/generator/OpenAPIGeneratorCatalog';
import {type LoadedSchema, loadSchemas} from '@fluxer/openapi/src/registry/SchemaLoader';
@@ -52,6 +53,11 @@ export async function loadSchemasIntoRegistry(
schemaRegistry.register(name, schema);
}
}
for (const [name, schema] of Object.entries(getRegisteredNamedObjectSchemas())) {
if (!schemaRegistry.has(name)) {
schemaRegistry.register(name, schema);
}
}
return {
loadedSchemas,
totalRegisteredSchemas: Object.keys(schemaRegistry.getAllSchemas()).length,
@@ -13,6 +13,7 @@ import {createBase64StringType} from '@fluxer/schema/src/primitives/FileValidato
import {
createBitflagInt32Type,
createInt32EnumType,
createNamedObject,
createNamedStringLiteralUnion,
createStringType,
Int32Type,
@@ -324,7 +325,7 @@ export const OAuth2ApplicationsMeResponse = z.union([ApplicationListResponse, Ap
export type OAuth2ApplicationsMeResponse = z.infer<typeof OAuth2ApplicationsMeResponse>;
const OAuth2AuthorizationResponse = z.object({
const OAuth2AuthorizationResponse = createNamedObject('OAuth2AuthorizationResponse', {
application: z
.object({
id: SnowflakeStringType.describe('The unique identifier of the application'),
@@ -316,6 +316,11 @@ export function createNamedStringLiteralUnion<T extends string>(
.describe(`fluxer:EnumValues:${JSON.stringify(entries)}${descPart}`);
}
export function createNamedObject<T extends z.ZodRawShape>(typeName: string, shape: T, description?: string) {
const descPart = description ? ` ${description}` : '';
return z.object(shape).describe(`fluxer:NamedObject:${typeName}${descPart}`);
}
type FlexibleStringLiteralUnionOperand<T extends string> = z.ZodLiteral<T> | z.ZodString;
type FlexibleStringLiteralUnionOperands<T extends string> = [
z.ZodLiteral<T>,