refactor progress
This commit is contained in:
168
packages/openapi/src/registry/ParameterRegistry.tsx
Normal file
168
packages/openapi/src/registry/ParameterRegistry.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {SnowflakeTypeRef} from '@fluxer/openapi/src/converters/BuiltInSchemas';
|
||||
import type {OpenAPIParameter} from '@fluxer/openapi/src/Types';
|
||||
|
||||
export const COMMON_PATH_PARAMETERS: Record<string, OpenAPIParameter> = {
|
||||
guild_id: {
|
||||
name: 'guild_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the guild',
|
||||
},
|
||||
channel_id: {
|
||||
name: 'channel_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the channel',
|
||||
},
|
||||
user_id: {
|
||||
name: 'user_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the user',
|
||||
},
|
||||
message_id: {
|
||||
name: 'message_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the message',
|
||||
},
|
||||
role_id: {
|
||||
name: 'role_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the role',
|
||||
},
|
||||
emoji_id: {
|
||||
name: 'emoji_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the emoji',
|
||||
},
|
||||
sticker_id: {
|
||||
name: 'sticker_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the sticker',
|
||||
},
|
||||
invite_code: {
|
||||
name: 'invite_code',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {type: 'string'},
|
||||
description: 'The invite code',
|
||||
},
|
||||
webhook_id: {
|
||||
name: 'webhook_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the webhook',
|
||||
},
|
||||
webhook_token: {
|
||||
name: 'webhook_token',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {type: 'string'},
|
||||
description: 'The webhook token',
|
||||
},
|
||||
pack_id: {
|
||||
name: 'pack_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the pack',
|
||||
},
|
||||
application_id: {
|
||||
name: 'application_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'The ID of the OAuth2 application',
|
||||
},
|
||||
};
|
||||
|
||||
export const COMMON_QUERY_PARAMETERS: Record<string, OpenAPIParameter> = {
|
||||
limit: {
|
||||
name: 'limit',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: {type: 'integer', minimum: 1, maximum: 100, default: 50},
|
||||
description: 'Maximum number of results to return',
|
||||
},
|
||||
before: {
|
||||
name: 'before',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'Get results before this ID',
|
||||
},
|
||||
after: {
|
||||
name: 'after',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'Get results after this ID',
|
||||
},
|
||||
around: {
|
||||
name: 'around',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: SnowflakeTypeRef,
|
||||
description: 'Get results around this ID',
|
||||
},
|
||||
};
|
||||
|
||||
export function extractPathParameters(path: string): Array<OpenAPIParameter> {
|
||||
const paramRegex = /:(\w+)/g;
|
||||
const parameters: Array<OpenAPIParameter> = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = paramRegex.exec(path)) !== null) {
|
||||
const paramName = match[1];
|
||||
const commonParam = COMMON_PATH_PARAMETERS[paramName];
|
||||
|
||||
if (commonParam) {
|
||||
parameters.push({...commonParam});
|
||||
} else {
|
||||
parameters.push({
|
||||
name: paramName,
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {type: 'string'},
|
||||
description: `The ${paramName.replace(/_/g, ' ')}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
export function convertPathToOpenAPI(path: string): string {
|
||||
return path.replace(/:(\w+)/g, '{$1}');
|
||||
}
|
||||
164
packages/openapi/src/registry/ResponseRegistry.tsx
Normal file
164
packages/openapi/src/registry/ResponseRegistry.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {OpenAPIResponse, OpenAPISchema} from '@fluxer/openapi/src/Types';
|
||||
|
||||
export const ERROR_SCHEMA: OpenAPISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
$ref: '#/components/schemas/APIErrorCode',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable error message',
|
||||
},
|
||||
errors: {
|
||||
type: 'array',
|
||||
description: 'Field-specific validation errors',
|
||||
items: {
|
||||
$ref: '#/components/schemas/ValidationErrorItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['code', 'message'],
|
||||
};
|
||||
|
||||
export const RATE_LIMIT_HEADERS: Record<string, {description: string; schema: OpenAPISchema}> = {
|
||||
'X-RateLimit-Limit': {
|
||||
description: 'The number of requests that can be made in the current window',
|
||||
schema: {type: 'integer'},
|
||||
},
|
||||
'X-RateLimit-Remaining': {
|
||||
description: 'The number of remaining requests that can be made',
|
||||
schema: {type: 'integer'},
|
||||
},
|
||||
'X-RateLimit-Reset': {
|
||||
description: 'Unix timestamp when the rate limit resets',
|
||||
schema: {type: 'integer'},
|
||||
},
|
||||
'Retry-After': {
|
||||
description: 'Number of seconds to wait before retrying (only on 429)',
|
||||
schema: {type: 'integer'},
|
||||
},
|
||||
};
|
||||
|
||||
export const COMMON_RESPONSES: Record<string, OpenAPIResponse> = {
|
||||
'400': {
|
||||
description: 'Bad Request - The request was malformed or contained invalid data',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {$ref: '#/components/schemas/Error'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': {
|
||||
description: 'Unauthorized - Authentication is required or the token is invalid',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {$ref: '#/components/schemas/Error'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'403': {
|
||||
description: 'Forbidden - You do not have permission to perform this action',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {$ref: '#/components/schemas/Error'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'404': {
|
||||
description: 'Not Found - The requested resource was not found',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {$ref: '#/components/schemas/Error'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'429': {
|
||||
description: 'Too Many Requests - You are being rate limited',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {type: 'string', enum: ['RATE_LIMITED']},
|
||||
message: {type: 'string'},
|
||||
retry_after: {type: 'number', description: 'Seconds to wait before retrying'},
|
||||
global: {type: 'boolean', description: 'Whether this is a global rate limit'},
|
||||
},
|
||||
required: ['code', 'message', 'retry_after'],
|
||||
},
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
'Retry-After': RATE_LIMIT_HEADERS['Retry-After'],
|
||||
'X-RateLimit-Limit': RATE_LIMIT_HEADERS['X-RateLimit-Limit'],
|
||||
'X-RateLimit-Remaining': RATE_LIMIT_HEADERS['X-RateLimit-Remaining'],
|
||||
'X-RateLimit-Reset': RATE_LIMIT_HEADERS['X-RateLimit-Reset'],
|
||||
},
|
||||
},
|
||||
'500': {
|
||||
description: 'Internal Server Error - An unexpected error occurred',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {$ref: '#/components/schemas/Error'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getErrorResponses(requiresAuth: boolean): Record<string, OpenAPIResponse> {
|
||||
const responses: Record<string, OpenAPIResponse> = {
|
||||
'400': COMMON_RESPONSES['400'],
|
||||
'429': COMMON_RESPONSES['429'],
|
||||
'500': COMMON_RESPONSES['500'],
|
||||
};
|
||||
|
||||
if (requiresAuth) {
|
||||
responses['401'] = COMMON_RESPONSES['401'];
|
||||
responses['403'] = COMMON_RESPONSES['403'];
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
export function getSuccessResponse(schemaRef: string | null): OpenAPIResponse {
|
||||
if (!schemaRef) {
|
||||
return {
|
||||
description: 'Success',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
description: 'Success',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {$ref: schemaRef},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getNoContentResponse(): OpenAPIResponse {
|
||||
return {
|
||||
description: 'No Content',
|
||||
};
|
||||
}
|
||||
137
packages/openapi/src/registry/SchemaLoader.tsx
Normal file
137
packages/openapi/src/registry/SchemaLoader.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import {setSchemaName, zodToOpenAPISchema} from '@fluxer/openapi/src/converters/ZodToOpenAPI';
|
||||
import type {OpenAPISchema} from '@fluxer/openapi/src/Types';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export interface LoadedSchema {
|
||||
name: string;
|
||||
zodSchema: z.ZodTypeAny;
|
||||
openAPISchema: OpenAPISchema;
|
||||
}
|
||||
|
||||
function discoverSchemaModules(rootDir: string): Array<string> {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: Array<string> = [];
|
||||
const stack: Array<string> = [rootDir];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentDir = stack.pop();
|
||||
if (!currentDir) break;
|
||||
|
||||
const entries = fs.readdirSync(currentDir, {withFileTypes: true});
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'tests' || entry.name === 'node_modules') continue;
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) continue;
|
||||
if (!entry.name.endsWith('.tsx')) continue;
|
||||
if (entry.name.endsWith('.test.tsx')) continue;
|
||||
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results.sort();
|
||||
}
|
||||
|
||||
function getModulePaths(basePath: string): Array<string> {
|
||||
const schemaDomains = path.join(basePath, 'packages', 'schema', 'src', 'domains');
|
||||
return discoverSchemaModules(schemaDomains);
|
||||
}
|
||||
|
||||
function isZodSchema(value: unknown): value is z.ZodTypeAny {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
'_def' in value &&
|
||||
typeof (value as {parse?: unknown}).parse === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadSchemas(basePath: string): Promise<Map<string, LoadedSchema>> {
|
||||
const schemas = new Map<string, LoadedSchema>();
|
||||
const collectedSchemas: Array<{name: string; zodSchema: z.ZodTypeAny}> = [];
|
||||
|
||||
const modulePaths = getModulePaths(basePath);
|
||||
|
||||
for (const modulePath of modulePaths) {
|
||||
try {
|
||||
const moduleExports = await import(modulePath);
|
||||
|
||||
for (const [exportName, exportValue] of Object.entries(moduleExports)) {
|
||||
if (exportName.startsWith('_')) {
|
||||
continue;
|
||||
}
|
||||
if (typeof exportValue === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isZodSchema(exportValue)) {
|
||||
collectedSchemas.push({name: exportName, zodSchema: exportValue});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not load schema module ${modulePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const {name, zodSchema} of collectedSchemas) {
|
||||
setSchemaName(zodSchema, name);
|
||||
}
|
||||
|
||||
for (const {name, zodSchema} of collectedSchemas) {
|
||||
const openAPISchemaOrRef = zodToOpenAPISchema(zodSchema);
|
||||
if ('$ref' in openAPISchemaOrRef) {
|
||||
throw new Error(`Top-level schema export must not be a $ref: ${name}`);
|
||||
}
|
||||
const openAPISchema: OpenAPISchema = openAPISchemaOrRef;
|
||||
schemas.set(name, {
|
||||
name,
|
||||
zodSchema,
|
||||
openAPISchema,
|
||||
});
|
||||
}
|
||||
|
||||
return schemas;
|
||||
}
|
||||
|
||||
export function getSchemaNames(basePath: string): Array<string> {
|
||||
const modulePaths = getModulePaths(basePath);
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const modulePath of modulePaths) {
|
||||
const parts = modulePath.split('/');
|
||||
const fileName = parts[parts.length - 1];
|
||||
names.push(fileName);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
78
packages/openapi/src/registry/SchemaRegistry.tsx
Normal file
78
packages/openapi/src/registry/SchemaRegistry.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {OpenAPIRef, OpenAPISchema} from '@fluxer/openapi/src/Types';
|
||||
|
||||
export class SchemaRegistry {
|
||||
private schemas: Map<string, OpenAPISchema> = new Map();
|
||||
private references: Set<string> = new Set();
|
||||
|
||||
register(name: string, schema: OpenAPISchema): void {
|
||||
this.schemas.set(name, schema);
|
||||
}
|
||||
|
||||
getRef(name: string): OpenAPIRef {
|
||||
this.references.add(name);
|
||||
return {$ref: `#/components/schemas/${name}`};
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
return this.schemas.has(name);
|
||||
}
|
||||
|
||||
get(name: string): OpenAPISchema | undefined {
|
||||
return this.schemas.get(name);
|
||||
}
|
||||
|
||||
getAllSchemas(): Record<string, OpenAPISchema> {
|
||||
const result: Record<string, OpenAPISchema> = {};
|
||||
for (const [name, schema] of this.schemas) {
|
||||
result[name] = schema;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getReferencedSchemas(): Record<string, OpenAPISchema> {
|
||||
const result: Record<string, OpenAPISchema> = {};
|
||||
for (const name of this.references) {
|
||||
const schema = this.schemas.get(name);
|
||||
if (schema) {
|
||||
result[name] = schema;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getUnreferencedSchemas(): Array<string> {
|
||||
const unreferenced: Array<string> = [];
|
||||
for (const name of this.schemas.keys()) {
|
||||
if (!this.references.has(name)) {
|
||||
unreferenced.push(name);
|
||||
}
|
||||
}
|
||||
return unreferenced;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.schemas.clear();
|
||||
this.references.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const globalSchemaRegistry = new SchemaRegistry();
|
||||
Reference in New Issue
Block a user