@autobe/agent
Version:
AI backend server code generator
278 lines (251 loc) • 10.4 kB
text/typescript
import { AutoBeOpenApi } from "@autobe/interface";
import { StringUtil } from "@autobe/utils";
import { NamingConvention } from "@typia/utils";
import { IValidation } from "typia";
import { AutoBeJsonSchemaValidator } from "../utils/AutoBeJsonSchemaValidator";
export namespace AutoBeInterfaceOperationProgrammer {
export const fix = (
operation: Pick<AutoBeOpenApi.IOperation, "method" | "responseBody">,
): void => {
if (operation.method === "delete" && operation.responseBody !== null)
operation.responseBody = null;
};
export const validate = (props: {
errors: IValidation.IError[];
accessor: string;
operation: Omit<
AutoBeOpenApi.IOperation,
"authorizationActor" | "authorizationType" | "prerequisites"
>;
}): void => {
// get method has request body
if (
props.operation.method === "get" &&
props.operation.requestBody !== null
)
props.errors.push({
path: `${props.accessor}.requestBody`,
expected: "null (GET method cannot have request body)",
value: props.operation.requestBody,
description: StringUtil.trim`
GET method cannot have request body per HTTP specification.
Fix: Change method to "patch" for complex queries,
or set requestBody to null.
`,
});
// operation name
if (NamingConvention.variable(props.operation.name) === false)
props.errors.push({
path: `${props.accessor}.name`,
expected: "<valid_variable_name>",
value: props.operation.name,
description: StringUtil.trim`
Operation name "${props.operation.name}" is not a valid
JavaScript variable name.
It will be used as a controller method name, so it must be
a valid identifier.
`,
});
else if (props.operation.name === "index") {
if (props.operation.method !== "patch")
props.errors.push({
path: `${props.accessor}.method`,
expected: `"patch" when operation name is "index", or change operation name to something else`,
value: props.operation.method,
description: StringUtil.trim`
Operation name "index" is reserved for getting list of resources,
or pagination of the resources.
Fix: Change method to "patch" when using "index" as operation name.
Otherwise, change operation name to something else.
`,
});
if (props.operation.responseBody === null)
props.errors.push({
path: `${props.accessor}.responseBody`,
expected: `AutoBeOpenApi.IResponseBody (typeName: "IPageIResource") when operation name is "index"`,
value: props.operation.responseBody,
description: StringUtil.trim`
Operation name "index" is reserved for getting list of resources,
so response body must be a paginated type "IPageIResource".
Fix: Change response body type to paginated type "IPageIResource",
or change operation name to something else.
`,
});
else if (
props.operation.responseBody.typeName.startsWith("IPage") === false
)
props.errors.push({
path: `${props.accessor}.responseBody.typeName`,
expected: `"IPage${props.operation.responseBody.typeName}", or change operation name to something else`,
value: props.operation.responseBody.typeName,
description: StringUtil.trim`
Operation name "index" is reserved for getting list of resources,
so response body type must be paginated type "IPageIResource".
Fix: Change response body type to paginated type "IPage${props.operation.responseBody.typeName}",
or change operation name to something else.
`,
});
}
// validate path parameters match with path
validatePathParameters({
errors: props.errors,
operation: props.operation,
accessor: props.accessor,
});
// validate types
if (props.operation.requestBody !== null) {
validatePrimitiveBody({
kind: "requestBody",
errors: props.errors,
path: `${props.accessor}.requestBody`,
body: props.operation.requestBody,
});
AutoBeJsonSchemaValidator.validateKey({
errors: props.errors,
path: `${props.accessor}.requestBody.typeName`,
key: props.operation.requestBody.typeName,
});
}
if (props.operation.responseBody !== null) {
validatePrimitiveBody({
kind: "responseBody",
errors: props.errors,
path: `${props.accessor}.responseBody`,
body: props.operation.responseBody,
});
AutoBeJsonSchemaValidator.validateKey({
errors: props.errors,
path: `${props.accessor}.responseBody.typeName`,
key: props.operation.responseBody.typeName,
});
}
};
const validatePathParameters = (props: {
errors: IValidation.IError[];
operation: Omit<
AutoBeOpenApi.IOperation,
"authorizationActor" | "authorizationType" | "prerequisites"
>;
accessor: string;
}): void => {
// Check parameter → path matching and uniqueness
const parameterNames = new Set<string>();
props.operation.parameters.forEach((p, i) => {
// Check if parameter exists in path
if (props.operation.path.includes(`{${p.name}}`) === false)
props.errors.push({
path: `${props.accessor}.parameters[${i}]`,
expected: `removed, or expressed in AutoBeOpenApi.IOperation.path`,
value: p,
description: StringUtil.trim`
Parameter "${p.name}" is defined but not used in path "${props.operation.path}".
Fix: Remove parameter at index ${i}, or add {${p.name}} to the path.
`,
});
parameterNames.add(p.name);
});
// Check for duplicate parameter names
if (parameterNames.size !== props.operation.parameters.length)
props.errors.push({
path: `${props.accessor}.parameters`,
expected: `All parameter names must be unique`,
value: props.operation.parameters,
description: StringUtil.trim`
Duplicate parameter names found: ${props.operation.parameters.length} parameters, but only ${parameterNames.size} unique names.
Parameters: ${props.operation.parameters.map((p, idx) => `[${idx}]="${p.name}"`).join(", ")}
Fix: Remove duplicate parameter definitions.
`,
});
const symbols: string[] = props.operation.path
.split("{")
.slice(1)
.map((s) => s.split("}")[0]);
// Check path → parameters matching
symbols.forEach((s) => {
if (props.operation.parameters.some((p) => p.name === s) === false)
props.errors.push({
path: `${props.accessor}.path`,
expected: `removed, or defined in AutoBeOpenApi.IOperation.parameters[]`,
value: s,
description: StringUtil.trim`
Path contains "{${s}}" but no corresponding parameter definition exists.
Current parameters: ${props.operation.parameters.length === 0 ? "[]" : `[${props.operation.parameters.map((p) => `"${p.name}"`).join(", ")}]`}
Fix: Add parameter definition for "${s}", or remove {${s}} from path.
`,
});
});
// Check for duplicate path parameters
const uniqueSymbols = new Set(symbols);
if (uniqueSymbols.size !== symbols.length)
props.errors.push({
path: `${props.accessor}.path`,
expected: `All path parameter names must be unique`,
value: props.operation.path,
description: StringUtil.trim`
Duplicate path parameter names: ${symbols.length} parameters,
but only ${uniqueSymbols.size} unique names in "${props.operation.path}".
Path parameters: ${symbols.map((s, idx) => `[${idx}]="{${s}}"`).join(", ")}
Fix: Rename duplicate parameters to be unique.
(e.g., {userId} and {postId} instead of {userId} twice).
`,
});
};
const validatePrimitiveBody = (props: {
kind: "requestBody" | "responseBody";
errors: IValidation.IError[];
path: string;
body: AutoBeOpenApi.IRequestBody | AutoBeOpenApi.IResponseBody;
}): void => {
if (props.body.typeName === "undefined" || props.body.typeName === "null")
props.errors.push({
path: props.path,
value: props.body,
expected: "null",
description: StringUtil.trim`
Type "${props.body.typeName}" is not valid for ${props.kind}.
Use null for empty ${props.kind}.
`,
});
else if (
props.body.typeName === "number" ||
props.body.typeName === "string" ||
props.body.typeName === "boolean"
)
props.errors.push({
path: `${props.path}.typeName`,
value: props.body.typeName,
expected: "An object reference type encapsulating the primitive type",
description: StringUtil.trim`
Primitive type "${props.body.typeName}" not allowed
for ${props.kind}.
Encapsulate in object type (e.g., I${props.body.typeName[0].toUpperCase()}${props.body.typeName.slice(1)}Value).
`,
});
else if (
props.body.typeName === "object" ||
props.body.typeName === "any" ||
props.body.typeName === "interface"
)
props.errors.push({
path: `${props.path}.typeName`,
value: props.body.typeName,
expected: "An object reference type",
description: StringUtil.trim`
Type "${props.body.typeName}" is a reserved word. Use a different type name.
`,
});
else if (props.body.typeName.startsWith("I") === false) {
props.errors.push({
path: `${props.path}.typeName`,
value: props.body.typeName,
expected: "Type name starting with 'I' (e.g., IUser, IProduct, IOrder)",
description: StringUtil.trim`
Type name "${props.body.typeName}" must start with 'I' prefix
per AutoBE naming convention.
Fix: Rename to "I${props.body.typeName.charAt(0).toUpperCase()}${props.body.typeName.slice(1)}"
(e.g., IUser, IProduct).
`,
});
}
};
}