mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(openapi): explicit object-component registration via createNamedObject (#1183)
Signed-off-by: Hampus Kraft <[email protected]>
This commit is contained in:
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user