Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(php): Support literal types #5991

Merged
merged 15 commits into from
Feb 13, 2025
13 changes: 13 additions & 0 deletions generators/php/base/src/context/PhpAttributeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ export class PhpAttributeMapper {
}
case "enumString":
return php.codeblock("'string'");
case "literal":
{
switch (type.internalType.value.internalType.type) {
case "string":
return php.codeblock("'string'");
case "boolean":
return php.codeblock("'bool'");
default:
assertNever(type.internalType.value.internalType);
}
}
// NOTE: The linter complains if we don't have this here
break;
default:
assertNever(type.internalType);
}
Expand Down
4 changes: 2 additions & 2 deletions generators/php/base/src/context/PhpTypeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ export class PhpTypeMapper {
public convertLiteral({ literal }: { literal: Literal }): php.Type {
switch (literal.type) {
case "boolean":
return php.Type.bool();
return php.Type.literalBoolean(literal.boolean);
case "string":
return php.Type.string();
return php.Type.literalString(literal.string);
}
}

Expand Down
56 changes: 55 additions & 1 deletion generators/php/codegen/src/ast/Type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assertNever } from "@fern-api/core-utils";

import { BasePhpCustomConfigSchema } from "../custom-config/BasePhpCustomConfigSchema";
import { ClassReference } from "./ClassReference";
import { TypeLiteral } from "./TypeLiteral";
import { AstNode } from "./core/AstNode";
import { GLOBAL_NAMESPACE } from "./core/Constant";
import { Writer } from "./core/Writer";
Expand All @@ -22,7 +23,8 @@ type InternalType =
| Reference
| String_
| TypeDict
| Union;
| Union
| Literal;

interface Int {
type: "int";
Expand Down Expand Up @@ -103,6 +105,27 @@ interface EnumString {
value: ClassReference;
}

type LiteralString = TypeLiteral & {
internalType: {
type: "string";
value: string;
};
};

type LiteralBoolean = TypeLiteral & {
internalType: {
type: "boolean";
value: boolean;
};
};

type LiteralValue = LiteralString | LiteralBoolean;

interface Literal {
type: "literal";
value: LiteralValue;
}

/* A PHP parameter to a method */
export class Type extends AstNode {
private constructor(public readonly internalType: InternalType) {
Expand Down Expand Up @@ -274,6 +297,22 @@ export class Type extends AstNode {
writer.write("string");
}
break;
case "literal":
if (comment) {
writer.writeNode(this.internalType.value);
} else {
switch (this.internalType.value.internalType.type) {
case "string":
writer.write("string");
break;
case "boolean":
writer.write("bool");
break;
default:
assertNever(this.internalType.value.internalType);
}
}
break;
default:
assertNever(this.internalType);
}
Expand Down Expand Up @@ -326,6 +365,7 @@ export class Type extends AstNode {
case "optional":
case "typeDict":
case "union":
case "literal":
throw new Error("Cannot get class reference for " + this.internalType.type);
default:
assertNever(this.internalType);
Expand Down Expand Up @@ -442,6 +482,20 @@ export class Type extends AstNode {
});
}

public static literalString(value: string): Type {
return new this({
type: "literal",
value: TypeLiteral.string(value) as LiteralString
});
}

public static literalBoolean(value: boolean): Type {
return new this({
type: "literal",
value: TypeLiteral.boolean(value) as LiteralBoolean
});
}

private static isAlreadyOptional(value: Type) {
return value.internalType.type === "optional";
}
Expand Down
6 changes: 5 additions & 1 deletion generators/php/codegen/src/ast/TypeLiteral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ export class TypeLiteral extends AstNode {
this.writeStringWithHeredoc({ writer, value: this.internalType.value });
break;
}
writer.write(`'${this.internalType.value.replaceAll("'", "\\'")}'`);
if (this.internalType.value.includes("'")) {
writer.write(`"${this.internalType.value.replaceAll('"', '\\"')}"`);
break;
}
writer.write(`'${this.internalType.value}'`);
ajgateno marked this conversation as resolved.
Show resolved Hide resolved
break;
}
case "unknown": {
Expand Down
72 changes: 57 additions & 15 deletions generators/php/model/src/union/UnionGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,14 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
}

private getDiscriminantField(): php.Field {
// TODO(ajgateno): Actually add the literals as a union rather than just string
const discriminantValues = [];
this.unionTypeDeclaration.types.forEach((variant) =>
discriminantValues.push(variant.discriminantValue.wireValue)
);
discriminantValues.push("_unknown");
return php.field({
name: this.context.getPropertyName(this.unionTypeDeclaration.discriminant.name),
type: php.Type.string(),
type: php.Type.union(discriminantValues.map((value) => php.Type.literalString(value))),
access: this.context.getPropertyAccess(),
readonly_: true
});
Expand Down Expand Up @@ -386,11 +390,19 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
return php.codeblock(`"JSON data is missing property '${propertyName.wireValue}'"`);
}

private getDeserializationTypeCheckErrorMessage(propertyName: NameAndWireValue, typeName: string): php.CodeBlock {
private getDeserliazationTypeCheckErrorMessage(propertyName: NameAndWireValue, type: php.Type): php.CodeBlock {
return php.codeblock((writer) => {
writer.write(
`"Expected property '${this.context.getPropertyName(propertyName.name)}' in JSON data to be ${typeName}, instead received " . `
);
if (type.internalType.type === "literal") {
writer.write(
`"Expected property '${this.context.getPropertyName(propertyName.name)}' in JSON data to be `
);
writer.writeNode(type.internalType.value);
writer.write(', instead received " . ');
} else {
writer.write(
`"Expected property '${this.context.getPropertyName(propertyName.name)}' in JSON data to be ${type.internalType.type}, instead received " . `
);
}
writer.writeNode(
php.invokeMethod({
method: "get_debug_type",
Expand Down Expand Up @@ -519,10 +531,35 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
throw new Error(
"Cannot get type check for type dict value because properties should never be typeDicts"
);
case "union":
throw new Error(
"Cannot get type check for type dict value because properties should never be raw unions"
);
case "union": {
const checks: php.CodeBlock[] = type.internalType.types.map((item, index) => {
return php.codeblock((writer) => {
if (index > 0) {
writer.write("|| ");
}
writer.writeNode(this.getTypeCheck(variableGetter, item));
});
});
return php.codeblock((writer) => {
if (checks.length > 1) {
writer.write("(");
}

checks.forEach((check) => writer.writeNode(check));

if (checks.length > 1) {
writer.write(")");
}
});
}
case "literal": {
const literalValue = type.internalType.value;
return php.codeblock((writer) => {
writer.writeNode(variableGetter);
writer.write(" === ");
writer.writeNode(literalValue);
});
}
default:
assertNever(type.internalType);
}
Expand Down Expand Up @@ -679,6 +716,7 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
case "typeDict":
case "enumString":
case "union":
case "literal":
return php.codeblock((writer) => {
writer.write("$result");
writer.write("['");
Expand Down Expand Up @@ -804,6 +842,7 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
case "typeDict":
case "enumString":
case "union":
case "literal":
return php.codeblock((writer) => {
writer.write("$value = ");
writer.writeNodeStatement(this.valueGetter());
Expand Down Expand Up @@ -832,6 +871,7 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
case "typeDict":
case "enumString":
case "union":
case "literal":
return false;
default:
assertNever(type.internalType);
Expand Down Expand Up @@ -1030,9 +1070,7 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
php.codeblock((_writer) => {
_writer.controlFlow("if", isNotType);
_writer.writeNodeStatement(
this.getErrorThrow(
this.getDeserializationTypeCheckErrorMessage(property.name, type.internalType.type)
)
this.getErrorThrow(this.getDeserliazationTypeCheckErrorMessage(property.name, type))
);
_writer.endControlFlow();
})
Expand Down Expand Up @@ -1084,7 +1122,7 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
php.codeblock((_writer) => {
_writer.controlFlow("if", isNotType);
_writer.writeNodeStatement(
this.getErrorThrow(this.getDeserializationTypeCheckErrorMessage(discriminant, "string"))
this.getErrorThrow(this.getDeserliazationTypeCheckErrorMessage(discriminant, php.Type.string()))
);
_writer.endControlFlow();
})
Expand Down Expand Up @@ -1179,7 +1217,10 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
_writer.controlFlow("if", isNotType);
_writer.writeNodeStatement(
this.getErrorThrow(
this.getDeserializationTypeCheckErrorMessage(variant.discriminantValue, "array")
this.getDeserliazationTypeCheckErrorMessage(
variant.discriminantValue,
php.Type.array(php.Type.mixed())
)
)
);
_writer.endControlFlow();
Expand Down Expand Up @@ -1233,6 +1274,7 @@ export class UnionGenerator extends FileGenerator<PhpFile, ModelCustomConfigSche
case "typeDict":
case "enumString":
case "union":
case "literal":
return php.codeblock((writer) => {
writer.write(`$args['${this.getValueFieldName()}'] = `);
writer.writeNodeStatement(discriminantGetter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
case "date":
case "dateTime":
case "mixed":
case "literal":
return this.decodeJsonResponseForPrimitive({
arguments_,
methodSuffix: upperFirst(internalType.type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export abstract class EndpointRequest {
case "null":
case "typeDict":
case "enumString":
case "literal":
return bodyArgument;
}
}
Expand Down
7 changes: 7 additions & 0 deletions generators/php/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
- version: 0.14.0
changelogEntry:
- type: feat
summary: >-
Support literal types
irVersion: 55

- version: 0.13.7
changelogEntry:
- type: fix
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading