Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ const RESERVED_NAMES = new Set([
"import",
"for",
"assert",
"switch",
"getClass"
"switch"
]);

// Method names that conflict with final methods in Java's Object class
const RESERVED_METHOD_NAMES = new Set(["getClass", "notify", "notifyAll", "wait"]);

export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGeneratorContext {
public ir: FernIr.dynamic.DynamicIntermediateRepresentation;
public customConfig: BaseJavaCustomConfigSchema;
Expand Down Expand Up @@ -74,7 +76,12 @@ export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGene
}

public getMethodName(name: FernIr.Name): string {
return this.getName(name.camelCase.safeName);
const methodName = name.camelCase.safeName;
// Use suffix for reserved method names to match Java v1 generator behavior
if (this.isReservedMethodName(methodName)) {
return methodName + "_";
}
return this.getName(methodName);
}

public getRootClientClassReference(): java.ClassReference {
Expand Down Expand Up @@ -446,4 +453,8 @@ export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGene
private isReservedName(name: string): boolean {
return RESERVED_NAMES.has(name);
}

private isReservedMethodName(name: string): boolean {
return RESERVED_METHOD_NAMES.has(name);
}
}
22 changes: 18 additions & 4 deletions generators/java-v2/sdk/src/readme/ReadmeSnippetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ This SDK supports two authentication methods:
If you already have a valid access token, you can use it directly:

\`\`\`java
${clientClassName} client = ${clientClassName}.withToken("your-access-token")
${clientClassName} client = ${clientClassName}.builder()
.token("your-access-token")
.url("https://api.example.com")
.build();
\`\`\`
Expand All @@ -205,7 +206,8 @@ ${clientClassName} client = ${clientClassName}.withToken("your-access-token")
The SDK can automatically handle token acquisition and refresh:

\`\`\`java
${clientClassName} client = ${clientClassName}.withCredentials("client-id", "client-secret")
${clientClassName} client = ${clientClassName}.builder()
.credentials("client-id", "client-secret")
.url("https://api.example.com")
.build();
\`\`\``;
Expand Down Expand Up @@ -808,7 +810,9 @@ ${clientClassName} client = ${clientClassName}.withCredentials("client-id", "cli
}

private getAccessFromRootClient(fernFilepath: FernFilepath): java.AstNode {
const clientAccessParts = fernFilepath.allParts.map((part) => part.camelCase.safeName + "()");
const clientAccessParts = fernFilepath.allParts.map(
(part) => this.getKeyWordCompatibleMethodName(part.camelCase.safeName) + "()"
);
return clientAccessParts.length > 0
? java.codeblock(`${ReadmeSnippetBuilder.CLIENT_VARIABLE_NAME}.${clientAccessParts.join(".")}`)
: java.codeblock(ReadmeSnippetBuilder.CLIENT_VARIABLE_NAME);
Expand Down Expand Up @@ -933,7 +937,8 @@ ${clientClassName} client = ${clientClassName}.withCredentials("client-id", "cli

// Get access path to WebSocket client from root client
const clientAccessParts = fernFilepath.allParts.map(
(part: { camelCase: { safeName: string } }) => part.camelCase.safeName + "()"
(part: { camelCase: { safeName: string } }) =>
this.getKeyWordCompatibleMethodName(part.camelCase.safeName) + "()"
);
const wsClientAccess =
clientAccessParts.length > 0
Expand Down Expand Up @@ -1077,4 +1082,13 @@ ${clientClassName} client = ${clientClassName}.withCredentials("client-id", "cli

return this.renderSnippet(snippet);
}

private static RESERVED_METHOD_NAMES = new Set(["getClass", "notify", "notifyAll", "wait"]);

private getKeyWordCompatibleMethodName(methodName: string): string {
if (ReadmeSnippetBuilder.RESERVED_METHOD_NAMES.has(methodName)) {
return methodName + "_";
}
return methodName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class KeyWordUtils {
"assert",
"switch");

private static final Set<String> RESERVED_METHOD_NAMES = Set.of("getClass");
private static final Set<String> RESERVED_METHOD_NAMES = Set.of("getClass", "notify", "notifyAll", "wait");

private KeyWordUtils() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.fern.java.output.GeneratedJavaFile;
import com.fern.java.output.GeneratedJavaInterface;
import com.fern.java.output.GeneratedObjectMapper;
import com.fern.java.utils.KeyWordUtils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
Expand Down Expand Up @@ -373,7 +374,8 @@ public Result buildClients() {
}

private MethodSpec.Builder getBaseSubpackageMethod(Subpackage subpackage, ClassName subpackageClientInterface) {
return MethodSpec.methodBuilder(subpackage.getName().getCamelCase().getSafeName())
return MethodSpec.methodBuilder(KeyWordUtils.getKeyWordCompatibleMethodName(
subpackage.getName().getCamelCase().getSafeName()))
.addModifiers(Modifier.PUBLIC)
.returns(subpackageClientInterface);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ public Boolean _visitUnknown(Object unknownType) {
// For staged builder, add static factory methods that delegate to builder class
ClassName tokenAuthClassName = builderName.nestedClass("_TokenAuth");
ClassName credentialsAuthClassName = builderName.nestedClass("_CredentialsAuth");
ClassName builderStageClassName = builderName.nestedClass("_Builder");

result.getClientImpl()
.addMethod(MethodSpec.methodBuilder("withToken")
Expand All @@ -307,6 +308,15 @@ public Boolean _visitUnknown(Object unknownType) {
.returns(credentialsAuthClassName)
.addStatement("return $T.withCredentials(clientId, clientSecret)", builderName)
.build());

result.getClientImpl()
.addMethod(MethodSpec.methodBuilder("builder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addJavadoc("Creates a new client builder.\n")
.addJavadoc("@return A builder for configuring and creating the client")
.returns(builderStageClassName)
.addStatement("return $T.builder()", builderName)
.build());
} else {
result.getClientImpl()
.addMethod(MethodSpec.methodBuilder("builder")
Expand Down Expand Up @@ -1464,6 +1474,191 @@ private void generateStagedBuilderForOAuth(

clientBuilder.addMethod(withCredentialsMethod.build());

// Add builder() factory method to base builder
ClassName builderStageClassName = builderName.nestedClass("_Builder");
clientBuilder.addMethod(MethodSpec.methodBuilder("builder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addJavadoc("Creates a new client builder.\n")
.addJavadoc("Use this method to start building a client with the classic builder pattern.\n")
.addJavadoc("\n")
.addJavadoc("@return A builder for configuring authentication and creating the client")
.returns(builderStageClassName)
.addStatement("return new _Builder()")
.build());

// Create _Builder nested class with all builder methods plus token() and credentials()
TypeSpec.Builder builderStageBuilder = TypeSpec.classBuilder("_Builder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);

// Add fields to store configuration values
builderStageBuilder.addField(FieldSpec.builder(generatedEnvironmentsClass.getClassName(), "environment")
.addModifiers(Modifier.PRIVATE)
.build());

builderStageBuilder.addField(FieldSpec.builder(
ParameterizedTypeName.get(ClassName.get(Optional.class), ClassName.get(Integer.class)),
"timeout")
.addModifiers(Modifier.PRIVATE)
.initializer("$T.empty()", Optional.class)
.build());

builderStageBuilder.addField(FieldSpec.builder(
ParameterizedTypeName.get(ClassName.get(Optional.class), ClassName.get(Integer.class)),
"maxRetries")
.addModifiers(Modifier.PRIVATE)
.initializer("$T.empty()", Optional.class)
.build());

builderStageBuilder.addField(FieldSpec.builder(OkHttpClient.class, "httpClient")
.addModifiers(Modifier.PRIVATE)
.build());

builderStageBuilder.addField(FieldSpec.builder(
ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(String.class)),
"headers")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("new $T<>()", HashMap.class)
.build());

// Add environment() method if environments are present
if (generatedEnvironmentsClass.optionsPresent()) {
builderStageBuilder.addMethod(MethodSpec.methodBuilder("environment")
.addModifiers(Modifier.PUBLIC)
.addParameter(generatedEnvironmentsClass.getClassName(), "environment")
.returns(builderStageClassName)
.addStatement("this.environment = environment")
.addStatement("return this")
.build());
}

// Add url() method if single URL environment
if (generatedEnvironmentsClass.info() instanceof SingleUrlEnvironmentClass) {
SingleUrlEnvironmentClass singleUrlEnvClass =
((SingleUrlEnvironmentClass) generatedEnvironmentsClass.info());
builderStageBuilder.addMethod(MethodSpec.methodBuilder("url")
.addModifiers(Modifier.PUBLIC)
.addParameter(String.class, "url")
.returns(builderStageClassName)
.addStatement(
"this.environment = $T.$N(url)",
generatedEnvironmentsClass.getClassName(),
singleUrlEnvClass.getCustomMethod())
.addStatement("return this")
.build());
}

// Add timeout() method
builderStageBuilder.addMethod(MethodSpec.methodBuilder("timeout")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Sets the timeout (in seconds) for the client. Defaults to 60 seconds.")
.addParameter(int.class, "timeout")
.returns(builderStageClassName)
.addStatement("this.timeout = $T.of(timeout)", Optional.class)
.addStatement("return this")
.build());

// Add maxRetries() method
builderStageBuilder.addMethod(MethodSpec.methodBuilder("maxRetries")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Sets the maximum number of retries for the client. Defaults to 2 retries.")
.addParameter(int.class, "maxRetries")
.returns(builderStageClassName)
.addStatement("this.maxRetries = $T.of(maxRetries)", Optional.class)
.addStatement("return this")
.build());

// Add httpClient() method
builderStageBuilder.addMethod(MethodSpec.methodBuilder("httpClient")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Sets the underlying OkHttp client")
.addParameter(OkHttpClient.class, "httpClient")
.returns(builderStageClassName)
.addStatement("this.httpClient = httpClient")
.addStatement("return this")
.build());

// Add addHeader() method
builderStageBuilder.addMethod(MethodSpec.methodBuilder("addHeader")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Add a custom header to be sent with all requests.\n")
.addJavadoc("@param name The header name\n")
.addJavadoc("@param value The header value\n")
.addJavadoc("@return This builder for method chaining")
.addParameter(String.class, "name")
.addParameter(String.class, "value")
.returns(builderStageClassName)
.addStatement("this.headers.put(name, value)")
.addStatement("return this")
.build());

// Add token() method that returns _TokenAuth with configuration copied using setter methods
MethodSpec.Builder tokenMethodBuilder = MethodSpec.methodBuilder("token")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Configure the client to use a pre-generated access token for authentication.\n")
.addJavadoc("Use this when you already have a valid access token and want to bypass\n")
.addJavadoc("the OAuth client credentials flow.\n")
.addJavadoc("\n")
.addJavadoc(
"@param $L The access token to use for Authorization header\n",
tokenOverridePropertyName)
.addJavadoc("@return A builder configured for token authentication")
.addParameter(String.class, tokenOverridePropertyName)
.returns(tokenAuthClassName)
.addStatement("_TokenAuth auth = new _TokenAuth($L)", tokenOverridePropertyName)
.beginControlFlow("if (this.environment != null)")
.addStatement("auth.environment = this.environment")
.endControlFlow()
.beginControlFlow("if (this.timeout.isPresent())")
.addStatement("auth.timeout(this.timeout.get())")
.endControlFlow()
.beginControlFlow("if (this.maxRetries.isPresent())")
.addStatement("auth.maxRetries(this.maxRetries.get())")
.endControlFlow()
.beginControlFlow("if (this.httpClient != null)")
.addStatement("auth.httpClient(this.httpClient)")
.endControlFlow()
.beginControlFlow("for ($T.Entry<String, String> header : this.headers.entrySet())", Map.class)
.addStatement("auth.addHeader(header.getKey(), header.getValue())")
.endControlFlow()
.addStatement("return auth");
builderStageBuilder.addMethod(tokenMethodBuilder.build());

// Add credentials() method that returns _CredentialsAuth with configuration copied using setter methods
MethodSpec.Builder credentialsMethodBuilder = MethodSpec.methodBuilder("credentials")
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Configure the client to use OAuth client credentials for authentication.\n")
.addJavadoc("The builder will automatically handle token acquisition and refresh.\n")
.addJavadoc("\n")
.addJavadoc("@param clientId The OAuth client ID\n")
.addJavadoc("@param clientSecret The OAuth client secret\n")
.addJavadoc("@return A builder configured for OAuth client credentials authentication")
.addParameter(String.class, "clientId")
.addParameter(String.class, "clientSecret")
.returns(credentialsAuthClassName)
.addStatement("_CredentialsAuth auth = new _CredentialsAuth(clientId, clientSecret)")
.beginControlFlow("if (this.environment != null)")
.addStatement("auth.environment = this.environment")
.endControlFlow()
.beginControlFlow("if (this.timeout.isPresent())")
.addStatement("auth.timeout(this.timeout.get())")
.endControlFlow()
.beginControlFlow("if (this.maxRetries.isPresent())")
.addStatement("auth.maxRetries(this.maxRetries.get())")
.endControlFlow()
.beginControlFlow("if (this.httpClient != null)")
.addStatement("auth.httpClient(this.httpClient)")
.endControlFlow()
.beginControlFlow("for ($T.Entry<String, String> header : this.headers.entrySet())", Map.class)
.addStatement("auth.addHeader(header.getKey(), header.getValue())")
.endControlFlow()
.addStatement("return auth");
builderStageBuilder.addMethod(credentialsMethodBuilder.build());

clientBuilder.addType(builderStageBuilder.build());

// Make configureAuthMethod empty for base class (subclasses override)
if (configureAuthMethod != null) {
// Base class setAuthentication is empty - subclasses provide the implementation
Expand Down
Loading
Loading