Skip to content

Commit

Permalink
feat(php): Generate base exceptions (#4719)
Browse files Browse the repository at this point in the history
  • Loading branch information
amckinney authored Sep 23, 2024
1 parent 3cd38a7 commit ec77c0f
Show file tree
Hide file tree
Showing 228 changed files with 5,976 additions and 863 deletions.
19 changes: 13 additions & 6 deletions generators/php/codegen/src/ast/Class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export class Class extends AstNode {
writer.indent();

this.writeFields({ writer, fields: orderByAccess(this.fields) });
if (this.constructor != null || this.methods.length > 0) {
writer.newLine();
}

if (this.constructor_ != null) {
this.writeConstructor({ writer, constructor: this.constructor_ });
Expand Down Expand Up @@ -137,18 +140,22 @@ export class Class extends AstNode {
}

private writeFields({ writer, fields }: { writer: Writer; fields: Field[] }): void {
for (const field of fields) {
fields.forEach((field, index) => {
if (index > 0) {
writer.newLine();
}
field.write(writer);
writer.writeNewLineIfLastLineNot();
writer.newLine();
}
});
}

private writeMethods({ writer, methods }: { writer: Writer; methods: Method[] }): void {
for (const method of methods) {
methods.forEach((method, index) => {
if (index > 0) {
writer.newLine();
}
method.write(writer);
writer.writeNewLineIfLastLineNot();
writer.newLine();
}
});
}
}
15 changes: 12 additions & 3 deletions generators/php/codegen/src/ast/Method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export declare namespace Method {
access: Access;
/* The parameters of the method */
parameters: Parameter[];
/* The exceptions that can be thrown, if any */
throws?: ClassReference[];
/* The return type of the method */
return_?: Type;
/* The body of the method */
Expand All @@ -29,17 +31,19 @@ export declare namespace Method {
export class Method extends AstNode {
public readonly name: string;
public readonly access: Access;
public readonly parameters: Parameter[] = [];
public readonly parameters: Parameter[];
public readonly throws: ClassReference[];
public readonly return_: Type | undefined;
public readonly body: CodeBlock | undefined;
public readonly docs: string | undefined;
public readonly classReference: ClassReference | undefined;

constructor({ name, access, parameters, return_, body, docs, classReference }: Method.Args) {
constructor({ name, access, parameters, throws, return_, body, docs, classReference }: Method.Args) {
super();
this.name = name;
this.access = access;
this.parameters = parameters;
this.throws = throws ?? [];
this.return_ = return_;
this.body = body;
this.docs = docs;
Expand Down Expand Up @@ -86,7 +90,12 @@ export class Method extends AstNode {
type: this.return_
});
}
// TODO: Write throws tags.
for (const throw_ of this.throws) {
comment.addTag({
tagType: "throws",
type: Type.reference(throw_)
});
}
writer.writeNode(comment);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ export abstract class AbstractPhpGeneratorContext<
return literal.type === "string" ? `'${literal.string}'` : literal.boolean ? "'true'" : "'false'";
}

public getThrowableClassReference(): php.ClassReference {
return php.classReference({
namespace: GLOBAL_NAMESPACE,
name: "Throwable"
});
}

public getDateTypeAttributeClassReference(): php.ClassReference {
return this.getCoreClassReference("DateType");
}
Expand Down
11 changes: 11 additions & 0 deletions generators/php/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { RootClientGenerator } from "./root-client/RootClientGenerator";
import { SubPackageClientGenerator } from "./subpackage-client/SubPackageClientGenerator";
import { WrappedEndpointRequestGenerator } from "./endpoint/request/WrappedEndpointRequestGenerator";
import { EnvironmentGenerator } from "./environment/EnvironmentGenerator";
import { BaseExceptionGenerator } from "./error/BaseExceptionGenerator";
import { BaseApiExceptionGenerator } from "./error/BaseApiExceptionGenerator";

export class SdkGeneratorCLI extends AbstractPhpGeneratorCli<SdkCustomConfigSchema, SdkGeneratorContext> {
protected constructContext({
Expand Down Expand Up @@ -50,6 +52,7 @@ export class SdkGeneratorCLI extends AbstractPhpGeneratorCli<SdkCustomConfigSche
this.generateRootClient(context);
this.generateSubpackages(context);
this.generateEnvironment(context);
this.generateErrors(context);
await context.project.persist();
}

Expand Down Expand Up @@ -104,4 +107,12 @@ export class SdkGeneratorCLI extends AbstractPhpGeneratorCli<SdkCustomConfigSche
const environmentGenerator = new EnvironmentGenerator(context);
environmentGenerator.generate();
}

private generateErrors(context: SdkGeneratorContext) {
const baseException = new BaseExceptionGenerator(context);
context.project.addSourceFiles(baseException.generate());

const baseApiException = new BaseApiExceptionGenerator(context);
context.project.addSourceFiles(baseApiException.generate());
}
}
24 changes: 15 additions & 9 deletions generators/php/sdk/src/SdkGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { camelCase, upperFirst } from "lodash-es";
import { RawClient } from "./core/RawClient";
import { GuzzleClient } from "./external/GuzzleClient";
import { ErrorId, ErrorDeclaration } from "@fern-fern/ir-sdk/api";
import { TYPES_DIRECTORY, ERRORS_DIRECTORY, REQUESTS_DIRECTORY } from "./constants";
import { EXCEPTIONS_DIRECTORY, TYPES_DIRECTORY, REQUESTS_DIRECTORY } from "./constants";
import { EndpointGenerator } from "./endpoint/EndpointGenerator";

export class SdkGeneratorContext extends AbstractPhpGeneratorContext<SdkCustomConfigSchema> {
Expand Down Expand Up @@ -100,18 +100,16 @@ export class SdkGeneratorContext extends AbstractPhpGeneratorContext<SdkCustomCo
}

public getBaseExceptionClassReference(): php.ClassReference {
// TODO: Update this to the generated base exception class.
return php.classReference({
name: "Exception",
namespace: this.getGlobalNamespace()
name: this.getOrganizationPascalCase() + "Exception",
namespace: this.getLocationForBaseException().namespace
});
}

public getBaseApiExceptionClassReference(): php.ClassReference {
// TODO: Update this to the generated base API exception class.
return php.classReference({
name: "Exception",
namespace: this.getGlobalNamespace()
name: this.getOrganizationPascalCase() + "ApiException",
namespace: this.getLocationForBaseException().namespace
});
}

Expand Down Expand Up @@ -292,7 +290,11 @@ export class SdkGeneratorContext extends AbstractPhpGeneratorContext<SdkCustomCo

public getLocationForErrorId(errorId: ErrorId): FileLocation {
const errorDeclaration = this.getErrorDeclarationOrThrow(errorId);
return this.getFileLocation(errorDeclaration.name.fernFilepath, ERRORS_DIRECTORY);
return this.getFileLocation(errorDeclaration.name.fernFilepath, EXCEPTIONS_DIRECTORY);
}

public getLocationForBaseException(): FileLocation {
return this.getFileLocation({ allParts: [], packagePath: [], file: undefined }, EXCEPTIONS_DIRECTORY);
}

private getDefaultBaseUrl(): php.CodeBlock {
Expand Down Expand Up @@ -327,6 +329,10 @@ export class SdkGeneratorContext extends AbstractPhpGeneratorContext<SdkCustomCo
}

private getComputedClientName(): string {
return `${upperFirst(camelCase(this.config.organization))}Client`;
return `${this.getOrganizationPascalCase()}Client`;
}

private getOrganizationPascalCase(): string {
return `${upperFirst(camelCase(this.config.organization))}`;
}
}
2 changes: 1 addition & 1 deletion generators/php/sdk/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const ERRORS_DIRECTORY = "Errors";
export const EXCEPTIONS_DIRECTORY = "Exceptions";
export const REQUESTS_DIRECTORY = "Requests";
export const TYPES_DIRECTORY = "Types";
79 changes: 70 additions & 9 deletions generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
parameters,
docs: endpoint.docs,
return_,
throws: [this.context.getBaseExceptionClassReference(), this.context.getBaseApiExceptionClassReference()],
body: php.codeblock((writer) => {
const queryParameterCodeBlock = endpointSignatureInfo.request?.getQueryParameterCodeBlock();
if (queryParameterCodeBlock != null) {
Expand Down Expand Up @@ -80,9 +81,11 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
writer.writeNode(this.context.getClientExceptionInterfaceClassReference());
writer.writeLine(" $e) {");
writer.indent();
writer.write("throw new ");
writer.writeNode(this.context.getBaseApiExceptionClassReference());
writer.writeTextStatement("($e->getMessage())");
writer.writeNodeStatement(
this.throwNewBaseException({
message: php.codeblock("$e->getMessage()")
})
);
writer.dedent();
writer.writeLine("}");

Expand All @@ -108,9 +111,12 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {

private getEndpointErrorHandling({ endpoint }: { endpoint: HttpEndpoint }): php.CodeBlock {
return php.codeblock((writer) => {
writer.write("throw new ");
writer.writeNode(this.context.getBaseApiExceptionClassReference());
writer.writeTextStatement(`("Error with status code " . ${STATUS_CODE_VARIABLE_NAME})`);
writer.writeNodeStatement(
this.throwNewBaseAPiException({
message: php.codeblock("'API request failed'"),
body: php.codeblock(`${RESPONSE_VARIABLE_NAME}->getBody()->getContents()`)
})
);
});
}

Expand Down Expand Up @@ -159,9 +165,11 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
writer.writeNode(this.context.getJsonExceptionClassReference());
writer.writeLine(" $e) {");
writer.indent();
writer.write("throw new ");
writer.writeNode(this.context.getBaseExceptionClassReference());
writer.writeTextStatement('("Failed to deserialize response", 0, $e)');
writer.writeNodeStatement(
this.throwNewBaseException({
message: php.codeblock('"Failed to deserialize response: {$e->getMessage()}"')
})
);
writer.dedent();
},
streaming: () => this.context.logger.error("Streaming not supported"),
Expand Down Expand Up @@ -282,4 +290,57 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
writer.write(`${JSON_VARIABLE_NAME} = ${RESPONSE_VARIABLE_NAME}->getBody()->getContents()`);
});
}

private throwNewBaseException({ message }: { message: php.CodeBlock }): php.CodeBlock {
return php.codeblock((writer) => {
writer.write("throw ");
writer.writeNode(
php.instantiateClass({
classReference: this.context.getBaseExceptionClassReference(),
arguments_: [
{
name: "message",
assignment: message
},
{
name: "previous",
assignment: php.codeblock("$e")
}
]
})
);
});
}

private throwNewBaseAPiException({
message,
body
}: {
message: php.CodeBlock;
body: php.CodeBlock;
}): php.CodeBlock {
return php.codeblock((writer) => {
writer.write("throw ");
writer.writeNode(
php.instantiateClass({
classReference: this.context.getBaseApiExceptionClassReference(),
arguments_: [
{
name: "message",
assignment: message
},
{
name: "statusCode",
assignment: php.codeblock(STATUS_CODE_VARIABLE_NAME)
},
{
name: "body",
assignment: body
}
],
multiline: true
})
);
});
}
}
97 changes: 97 additions & 0 deletions generators/php/sdk/src/error/BaseApiExceptionGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { php, PhpFile, FileGenerator } from "@fern-api/php-codegen";
import { join, RelativeFilePath } from "@fern-api/fs-utils";
import { SdkCustomConfigSchema } from "../SdkCustomConfig";
import { SdkGeneratorContext } from "../SdkGeneratorContext";

export class BaseApiExceptionGenerator extends FileGenerator<PhpFile, SdkCustomConfigSchema, SdkGeneratorContext> {
public doGenerate(): PhpFile {
const class_ = php.class_({
...this.context.getBaseApiExceptionClassReference(),
parentClassReference: this.context.getBaseExceptionClassReference(),
docs: "This exception type will be thrown for any non-2XX API responses."
});

class_.addField(
php.field({
name: "body",
type: php.Type.mixed(),
access: "private"
})
);

class_.addConstructor(this.getConstructorMethod());
class_.addMethod(this.getBodyGetterMethod());
class_.addMethod(this.getToStringMethod());

return new PhpFile({
clazz: class_,
directory: this.context.getLocationForBaseException().directory,
rootNamespace: this.context.getRootNamespace(),
customConfig: this.context.customConfig
});
}

private getConstructorMethod(): php.Class.Constructor {
const parameters: php.Parameter[] = [
php.parameter({
name: "message",
type: php.Type.string()
}),
php.parameter({
name: "statusCode",
type: php.Type.int()
}),
php.parameter({
name: "body",
type: php.Type.mixed()
}),
php.parameter({
name: "previous",
type: php.Type.optional(php.Type.reference(this.context.getThrowableClassReference())),
initializer: php.codeblock("null")
})
];
return {
access: "public",
parameters,
body: php.codeblock((writer) => {
writer.writeTextStatement("$this->body = $body");
writer.writeTextStatement("parent::__construct($message, $statusCode, $previous)");
})
};
}

private getToStringMethod(): php.Method {
return php.method({
name: "__toString",
access: "public",
parameters: [],
return_: php.Type.string(),
body: php.codeblock((writer) => {
writer.controlFlow("if", php.codeblock("empty($this->body)"));
writer.writeTextStatement('return "$this->message; Status Code: $this->code\\n"');
writer.endControlFlow();
writer.writeTextStatement(
'return "$this->message; Status Code: $this->code; Body: " . $this->body . "\\n"'
);
})
});
}

private getBodyGetterMethod(): php.Method {
return php.method({
name: "getBody",
access: "public",
parameters: [],
return_: php.Type.mixed(),
docs: "Returns the body of the response that triggered the exception.",
body: php.codeblock((writer) => {
writer.writeTextStatement("return $this->body");
})
});
}

protected getFilepath(): RelativeFilePath {
return join(RelativeFilePath.of(`${this.context.getBaseApiExceptionClassReference().name}.php`));
}
}
Loading

0 comments on commit ec77c0f

Please sign in to comment.