diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index 7fee1ae2c..2f4e291a0 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -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", diff --git a/packages/openapi/src/converters/ZodToOpenAPI.ts b/packages/openapi/src/converters/ZodToOpenAPI.ts index 305491fff..9d584b162 100644 --- a/packages/openapi/src/converters/ZodToOpenAPI.ts +++ b/packages/openapi/src/converters/ZodToOpenAPI.ts @@ -119,6 +119,23 @@ export function getRegisteredDiscriminatedUnionBranchSchemas(): Record(); + +export function getRegisteredNamedObjectSchemas(): Record { + const result: Record = {}; + 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 = {}; const required: Array = []; 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': diff --git a/packages/openapi/src/converters/ZodToOpenAPIAnnotationParser.ts b/packages/openapi/src/converters/ZodToOpenAPIAnnotationParser.ts index 5909e688a..f1c9f005a 100644 --- a/packages/openapi/src/converters/ZodToOpenAPIAnnotationParser.ts +++ b/packages/openapi/src/converters/ZodToOpenAPIAnnotationParser.ts @@ -16,6 +16,7 @@ export interface FluxerTypeAnnotation { enumEntries?: Array; bitflagValues?: Array; 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); diff --git a/packages/openapi/src/generator/OpenAPISchemaRegistryLoader.ts b/packages/openapi/src/generator/OpenAPISchemaRegistryLoader.ts index 43db48683..5221a52ee 100644 --- a/packages/openapi/src/generator/OpenAPISchemaRegistryLoader.ts +++ b/packages/openapi/src/generator/OpenAPISchemaRegistryLoader.ts @@ -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, diff --git a/packages/schema/src/domains/oauth/OAuthSchemas.ts b/packages/schema/src/domains/oauth/OAuthSchemas.ts index 92b872568..988235d04 100644 --- a/packages/schema/src/domains/oauth/OAuthSchemas.ts +++ b/packages/schema/src/domains/oauth/OAuthSchemas.ts @@ -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; -const OAuth2AuthorizationResponse = z.object({ +const OAuth2AuthorizationResponse = createNamedObject('OAuth2AuthorizationResponse', { application: z .object({ id: SnowflakeStringType.describe('The unique identifier of the application'), diff --git a/packages/schema/src/primitives/SchemaPrimitives.ts b/packages/schema/src/primitives/SchemaPrimitives.ts index fc262a366..ee95ba32c 100644 --- a/packages/schema/src/primitives/SchemaPrimitives.ts +++ b/packages/schema/src/primitives/SchemaPrimitives.ts @@ -316,6 +316,11 @@ export function createNamedStringLiteralUnion( .describe(`fluxer:EnumValues:${JSON.stringify(entries)}${descPart}`); } +export function createNamedObject(typeName: string, shape: T, description?: string) { + const descPart = description ? ` ${description}` : ''; + return z.object(shape).describe(`fluxer:NamedObject:${typeName}${descPart}`); +} + type FlexibleStringLiteralUnionOperand = z.ZodLiteral | z.ZodString; type FlexibleStringLiteralUnionOperands = [ z.ZodLiteral,