diff --git a/generators/java-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/java-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 9c70d2ea8c45..2fd486bfae84 100644 --- a/generators/java-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/java-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -439,10 +439,24 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.BasicAuth; values: FernIr.dynamic.BasicAuthValues; }): java.BuilderParameter[] { + // usernameOmit/passwordOmit may exist in newer IR versions + const authRecord = auth as unknown as Record; + const usernameOmitted = !!authRecord.usernameOmit; + const passwordOmitted = !!authRecord.passwordOmit; + const credentialParts: string[] = []; + if (!usernameOmitted) { + credentialParts.push(`"${values.username}"`); + } + if (!passwordOmitted) { + credentialParts.push(`"${values.password}"`); + } + if (credentialParts.length === 0) { + return []; + } return [ { name: "credentials", - value: java.TypeLiteral.raw(`"${values.username}", "${values.password}"`) + value: java.TypeLiteral.raw(credentialParts.join(", ")) } ]; } diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/AbstractRootClientGenerator.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/AbstractRootClientGenerator.java index 25e15100cbbb..d11d2d7cb5c2 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/AbstractRootClientGenerator.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/AbstractRootClientGenerator.java @@ -1191,6 +1191,8 @@ private class AuthProviderInfo { final boolean isBasicAuth; final boolean isOAuth; final boolean isInferredAuth; + final boolean fieldNameOmitted; + final boolean secondaryFieldNameOmitted; AuthProviderInfo( String schemeKey, @@ -1199,7 +1201,17 @@ private class AuthProviderInfo { String secondaryFieldName, String envVarHint, boolean isBasicAuth) { - this(schemeKey, providerClass, fieldName, secondaryFieldName, envVarHint, isBasicAuth, false, false); + this( + schemeKey, + providerClass, + fieldName, + secondaryFieldName, + envVarHint, + isBasicAuth, + false, + false, + false, + false); } AuthProviderInfo( @@ -1211,6 +1223,30 @@ private class AuthProviderInfo { boolean isBasicAuth, boolean isOAuth, boolean isInferredAuth) { + this( + schemeKey, + providerClass, + fieldName, + secondaryFieldName, + envVarHint, + isBasicAuth, + isOAuth, + isInferredAuth, + false, + false); + } + + AuthProviderInfo( + String schemeKey, + String providerClass, + String fieldName, + String secondaryFieldName, + String envVarHint, + boolean isBasicAuth, + boolean isOAuth, + boolean isInferredAuth, + boolean fieldNameOmitted, + boolean secondaryFieldNameOmitted) { this.schemeKey = schemeKey; this.providerClass = providerClass; this.fieldName = fieldName; @@ -1219,6 +1255,8 @@ private class AuthProviderInfo { this.isBasicAuth = isBasicAuth; this.isOAuth = isOAuth; this.isInferredAuth = isInferredAuth; + this.fieldNameOmitted = fieldNameOmitted; + this.secondaryFieldNameOmitted = secondaryFieldNameOmitted; } } @@ -1277,66 +1315,87 @@ public Void visitBearer(BearerAuthScheme bearer) { @Override public Void visitBasic(BasicAuthScheme basic) { - // username + boolean usernameOmitted = basic.getUsernameOmit().orElse(false); + boolean passwordOmitted = basic.getPasswordOmit().orElse(false); + + // When omit is true, the field is completely removed from the end-user API. + // Only add non-omitted fields as builder fields. String usernameFieldName = NameUtils.toName(basic.getUsername()).getCamelCase().getSafeName(); - FieldSpec.Builder usernameField = - FieldSpec.builder(String.class, usernameFieldName).addModifiers(Modifier.PRIVATE); - if (basic.getUsernameEnvVar().isPresent()) { - usernameField.initializer( - "System.getenv($S)", basic.getUsernameEnvVar().get().get()); - } else { - usernameField.initializer("null"); - } - this.clientBuilder.addField(usernameField.build()); - String passwordFieldName = NameUtils.toName(basic.getPassword()).getCamelCase().getSafeName(); - FieldSpec.Builder passwordField = - FieldSpec.builder(String.class, passwordFieldName).addModifiers(Modifier.PRIVATE); - if (basic.getPasswordEnvVar().isPresent()) { - passwordField.initializer( - "System.getenv($S)", basic.getPasswordEnvVar().get().get()); - } else { - passwordField.initializer("null"); + + if (!usernameOmitted) { + FieldSpec.Builder usernameField = + FieldSpec.builder(String.class, usernameFieldName).addModifiers(Modifier.PRIVATE); + if (basic.getUsernameEnvVar().isPresent()) { + usernameField.initializer( + "System.getenv($S)", basic.getUsernameEnvVar().get().get()); + } else { + usernameField.initializer("null"); + } + this.clientBuilder.addField(usernameField.build()); } - this.clientBuilder.addField(passwordField.build()); - ParameterSpec usernameParam = - ParameterSpec.builder(String.class, usernameFieldName).build(); - ParameterSpec passwordParam = - ParameterSpec.builder(String.class, passwordFieldName).build(); - this.clientBuilder.addMethod(MethodSpec.methodBuilder("credentials") - .addModifiers(Modifier.PUBLIC) - .addParameter(usernameParam) - .addParameter(passwordParam) - .addStatement("this.$L = $L", usernameFieldName, usernameFieldName) - .addStatement("this.$L = $L", passwordFieldName, passwordFieldName) - .addStatement(isExtensible ? "return self()" : "return this") - .returns(isExtensible ? TypeVariableName.get("T") : builderName) - .build()); + if (!passwordOmitted) { + FieldSpec.Builder passwordField = + FieldSpec.builder(String.class, passwordFieldName).addModifiers(Modifier.PRIVATE); + if (basic.getPasswordEnvVar().isPresent()) { + passwordField.initializer( + "System.getenv($S)", basic.getPasswordEnvVar().get().get()); + } else { + passwordField.initializer("null"); + } + this.clientBuilder.addField(passwordField.build()); + } + // Only add credentials() method with non-omitted fields as parameters + MethodSpec.Builder credentialsMethodBuilder = + MethodSpec.methodBuilder("credentials").addModifiers(Modifier.PUBLIC); + if (!usernameOmitted) { + credentialsMethodBuilder.addParameter( + ParameterSpec.builder(String.class, usernameFieldName).build()); + credentialsMethodBuilder.addStatement("this.$L = $L", usernameFieldName, usernameFieldName); + } + if (!passwordOmitted) { + credentialsMethodBuilder.addParameter( + ParameterSpec.builder(String.class, passwordFieldName).build()); + credentialsMethodBuilder.addStatement("this.$L = $L", passwordFieldName, passwordFieldName); + } + // Only add credentials() method if at least one field is present + if (!usernameOmitted || !passwordOmitted) { + credentialsMethodBuilder + .addStatement(isExtensible ? "return self()" : "return this") + .returns(isExtensible ? TypeVariableName.get("T") : builderName); + this.clientBuilder.addMethod(credentialsMethodBuilder.build()); + } + + // build() validation: only check non-omitted fields if (isMandatory) { - this.buildMethod - .beginControlFlow("if (this.$L == null)", usernameFieldName) - .addStatement( - "throw new RuntimeException($S)", - basic.getUsernameEnvVar().isEmpty() - ? getErrorMessage(usernameFieldName) - : getErrorMessage( - usernameFieldName, - basic.getUsernameEnvVar().get())) - .endControlFlow(); - this.buildMethod - .beginControlFlow("if (this.$L == null)", passwordFieldName) - .addStatement( - "throw new RuntimeException($S)", - basic.getPasswordEnvVar().isEmpty() - ? getErrorMessage(passwordFieldName) - : getErrorMessage( - passwordFieldName, - basic.getPasswordEnvVar().get())) - .endControlFlow(); + if (!usernameOmitted) { + this.buildMethod + .beginControlFlow("if (this.$L == null)", usernameFieldName) + .addStatement( + "throw new RuntimeException($S)", + basic.getUsernameEnvVar().isEmpty() + ? getErrorMessage(usernameFieldName) + : getErrorMessage( + usernameFieldName, + basic.getUsernameEnvVar().get())) + .endControlFlow(); + } + if (!passwordOmitted) { + this.buildMethod + .beginControlFlow("if (this.$L == null)", passwordFieldName) + .addStatement( + "throw new RuntimeException($S)", + basic.getPasswordEnvVar().isEmpty() + ? getErrorMessage(passwordFieldName) + : getErrorMessage( + passwordFieldName, + basic.getPasswordEnvVar().get())) + .endControlFlow(); + } } // For ENDPOINT_SECURITY, track auth info instead of adding headers directly @@ -1348,20 +1407,40 @@ public Void visitBasic(BasicAuthScheme basic) { + basic.getPasswordEnvVar().get().get() + " environment variables"; } authProviderInfos.add(new AuthProviderInfo( - "Basic", "BasicAuthProvider", usernameFieldName, passwordFieldName, envVarHint, true)); + "Basic", + "BasicAuthProvider", + usernameFieldName, + passwordFieldName, + envVarHint, + true, + false, + false, + usernameOmitted, + passwordOmitted)); } else if (this.configureAuthMethod != null) { - this.configureAuthMethod - .beginControlFlow( - "if (this.$L != null && this.$L != null)", usernameFieldName, passwordFieldName) - .addStatement( - "String unencodedToken = this.$L + \":\" + this.$L", - usernameFieldName, - passwordFieldName) - .addStatement( - "String encodedToken = $T.getEncoder().encodeToString(unencodedToken.getBytes())", - Base64.class) - .addStatement("builder.addHeader($S, $S + encodedToken)", "Authorization", "Basic ") - .endControlFlow(); + // Condition: only require non-omitted fields to be present + if (!usernameOmitted && !passwordOmitted) { + this.configureAuthMethod.beginControlFlow( + "if (this.$L != null && this.$L != null)", usernameFieldName, passwordFieldName); + } else if (usernameOmitted && !passwordOmitted) { + this.configureAuthMethod.beginControlFlow("if (this.$L != null)", passwordFieldName); + } else if (!usernameOmitted && passwordOmitted) { + this.configureAuthMethod.beginControlFlow("if (this.$L != null)", usernameFieldName); + } else { + // Both fields omitted — skip auth header entirely when auth is non-mandatory + } + if (!usernameOmitted || !passwordOmitted) { + // Use empty string for omitted fields in token construction + String usernameRef = usernameOmitted ? "\"\"" : "this." + usernameFieldName; + String passwordRef = passwordOmitted ? "\"\"" : "this." + passwordFieldName; + this.configureAuthMethod + .addStatement("String unencodedToken = " + usernameRef + " + \":\" + " + passwordRef) + .addStatement( + "String encodedToken = $T.getEncoder().encodeToString(unencodedToken.getBytes())", + Base64.class) + .addStatement("builder.addHeader($S, $S + encodedToken)", "Authorization", "Basic ") + .endControlFlow(); + } } return null; } @@ -2342,16 +2421,42 @@ public void addRoutingAuthProviderSetup() { if (info.isBasicAuth) { String usernameField = info.fieldName; String passwordField = info.secondaryFieldName; - this.configureAuthMethod - .beginControlFlow("if (this.$L != null && this.$L != null)", usernameField, passwordField) - .addStatement( - "routingBuilder.addAuthProvider($S, new $T(() -> this.$L, () -> this.$L), $S)", - info.schemeKey, - providerClassName, - usernameField, - passwordField, - info.envVarHint) - .endControlFlow(); + boolean usernameOmit = info.fieldNameOmitted; + boolean passwordOmit = info.secondaryFieldNameOmitted; + // Build condition: only check non-omitted fields + String condition; + if (!usernameOmit && !passwordOmit) { + condition = "this." + usernameField + " != null && this." + passwordField + " != null"; + } else if (usernameOmit && !passwordOmit) { + condition = "this." + passwordField + " != null"; + } else if (!usernameOmit && passwordOmit) { + condition = "this." + usernameField + " != null"; + } else { + // Both fields omitted — skip auth provider entirely + condition = null; + } + if (condition != null) { + // Build constructor args: only pass args for non-omitted fields + // to match BasicAuthProvider's constructor signature + StringBuilder constructorArgs = new StringBuilder(); + if (!usernameOmit) { + constructorArgs.append("() -> this.").append(usernameField); + } + if (!passwordOmit) { + if (constructorArgs.length() > 0) { + constructorArgs.append(", "); + } + constructorArgs.append("() -> this.").append(passwordField); + } + this.configureAuthMethod + .beginControlFlow("if (" + condition + ")") + .addStatement( + "routingBuilder.addAuthProvider($S, new $T(" + constructorArgs + "), $S)", + info.schemeKey, + providerClassName, + info.envVarHint) + .endControlFlow(); + } } else if (info.isOAuth) { // OAuth requires creating an auth client and using OAuthAuthProvider ClassName clientOptionsClassName = generatedClientOptions.getClassName(); diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/auth/BasicAuthProviderGenerator.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/auth/BasicAuthProviderGenerator.java index 916a0609ba53..82283b24faa8 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/auth/BasicAuthProviderGenerator.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/auth/BasicAuthProviderGenerator.java @@ -60,21 +60,33 @@ public GeneratedJavaFile generateFile() { ParameterizedTypeName stringSupplierType = ParameterizedTypeName.get(ClassName.get(Supplier.class), ClassName.get(String.class)); - FieldSpec usernameSupplierField = FieldSpec.builder( - stringSupplierType, "usernameSupplier", Modifier.PRIVATE, Modifier.FINAL) - .build(); - FieldSpec passwordSupplierField = FieldSpec.builder( - stringSupplierType, "passwordSupplier", Modifier.PRIVATE, Modifier.FINAL) - .build(); + boolean usernameOmitted = basicAuthScheme.getUsernameOmit().orElse(false); + boolean passwordOmitted = basicAuthScheme.getPasswordOmit().orElse(false); + + FieldSpec usernameSupplierField = usernameOmitted + ? null + : FieldSpec.builder(stringSupplierType, "usernameSupplier", Modifier.PRIVATE, Modifier.FINAL) + .build(); + FieldSpec passwordSupplierField = passwordOmitted + ? null + : FieldSpec.builder(stringSupplierType, "passwordSupplier", Modifier.PRIVATE, Modifier.FINAL) + .build(); String usernameEnvVar = basicAuthScheme.getUsernameEnvVar().map(ev -> ev.get()).orElse(null); String passwordEnvVar = basicAuthScheme.getPasswordEnvVar().map(ev -> ev.get()).orElse(null); - String errorMessage = "Please provide username and password when initializing the client"; + String errorMessage; + if (usernameOmitted && !passwordOmitted) { + errorMessage = "Please provide password when initializing the client"; + } else if (!usernameOmitted && passwordOmitted) { + errorMessage = "Please provide username when initializing the client"; + } else { + errorMessage = "Please provide username and password when initializing the client"; + } - TypeSpec basicAuthProviderClass = TypeSpec.classBuilder(className) + TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addSuperinterface(authProviderClassName) .addJavadoc("Auth provider for Basic authentication.\n") @@ -89,16 +101,23 @@ public GeneratedJavaFile generateFile() { Modifier.STATIC, Modifier.FINAL) .initializer("$S", errorMessage) - .build()) - .addField(usernameSupplierField) - .addField(passwordSupplierField) - .addMethod(MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addParameter(stringSupplierType, "usernameSupplier") - .addParameter(stringSupplierType, "passwordSupplier") - .addStatement("this.$N = usernameSupplier", usernameSupplierField) - .addStatement("this.$N = passwordSupplier", passwordSupplierField) - .build()) + .build()); + + // Only add fields and constructor params for non-omitted fields + MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); + if (!usernameOmitted) { + classBuilder.addField(usernameSupplierField); + constructorBuilder.addParameter(stringSupplierType, "usernameSupplier"); + constructorBuilder.addStatement("this.$N = usernameSupplier", usernameSupplierField); + } + if (!passwordOmitted) { + classBuilder.addField(passwordSupplierField); + constructorBuilder.addParameter(stringSupplierType, "passwordSupplier"); + constructorBuilder.addStatement("this.$N = passwordSupplier", passwordSupplierField); + } + + TypeSpec basicAuthProviderClass = classBuilder + .addMethod(constructorBuilder.build()) .addMethod(buildGetAuthHeaders(endpointMetadataClassName, usernameSupplierField, passwordSupplierField)) .addMethod(buildCanCreateMethod(usernameEnvVar, passwordEnvVar)) .build(); @@ -114,18 +133,37 @@ public GeneratedJavaFile generateFile() { private MethodSpec buildGetAuthHeaders( ClassName endpointMetadataClassName, FieldSpec usernameSupplierField, FieldSpec passwordSupplierField) { - return MethodSpec.methodBuilder("getAuthHeaders") + boolean usernameOmitted = basicAuthScheme.getUsernameOmit().orElse(false); + boolean passwordOmitted = basicAuthScheme.getPasswordOmit().orElse(false); + + MethodSpec.Builder builder = MethodSpec.methodBuilder("getAuthHeaders") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(endpointMetadataClassName, "endpointMetadata") - .returns(ParameterizedTypeName.get(Map.class, String.class, String.class)) - .addStatement("String username = $N.get()", usernameSupplierField) - .addStatement("String password = $N.get()", passwordSupplierField) - .beginControlFlow("if (username == null || password == null)") - .addStatement("throw new $T(AUTH_CONFIG_ERROR_MESSAGE)", RuntimeException.class) - .endControlFlow() - .addStatement("String credentials = username + \":\" + password") - .addStatement( + .returns(ParameterizedTypeName.get(Map.class, String.class, String.class)); + + // Get values: omitted fields use empty string directly, non-omitted fields read from supplier + if (usernameOmitted) { + builder.addStatement("String username = \"\""); + } else { + builder.addStatement("String username = $N.get()", usernameSupplierField); + builder.beginControlFlow("if (username == null)") + .addStatement("throw new $T(AUTH_CONFIG_ERROR_MESSAGE)", RuntimeException.class) + .endControlFlow(); + } + + if (passwordOmitted) { + builder.addStatement("String password = \"\""); + } else { + builder.addStatement("String password = $N.get()", passwordSupplierField); + builder.beginControlFlow("if (password == null)") + .addStatement("throw new $T(AUTH_CONFIG_ERROR_MESSAGE)", RuntimeException.class) + .endControlFlow(); + } + + builder.addStatement("String credentials = username + \":\" + password"); + + return builder.addStatement( "String encoded = $T.getEncoder().encodeToString(credentials.getBytes($T.UTF_8))", Base64.class, StandardCharsets.class) @@ -136,26 +174,43 @@ private MethodSpec buildGetAuthHeaders( } private MethodSpec buildCanCreateMethod(String usernameEnvVar, String passwordEnvVar) { + boolean usernameOmitted = basicAuthScheme.getUsernameOmit().orElse(false); + boolean passwordOmitted = basicAuthScheme.getPasswordOmit().orElse(false); + ParameterizedTypeName stringSupplierType = ParameterizedTypeName.get(ClassName.get(Supplier.class), ClassName.get(String.class)); + // Only non-omitted fields appear as parameters MethodSpec.Builder builder = MethodSpec.methodBuilder("canCreate") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addJavadoc("Checks if this provider can be created with the given suppliers.\n") - .addParameter(stringSupplierType, "usernameSupplier") - .addParameter(stringSupplierType, "passwordSupplier") .returns(boolean.class); + // Build per-field checks: omitted fields are always satisfied (true) StringBuilder condition = new StringBuilder(); - condition.append("(usernameSupplier != null"); - if (usernameEnvVar != null) { - condition.append(" || System.getenv(\"").append(usernameEnvVar).append("\") != null"); + if (!usernameOmitted) { + builder.addParameter(stringSupplierType, "usernameSupplier"); + condition.append("(usernameSupplier != null"); + if (usernameEnvVar != null) { + condition.append(" || System.getenv(\"").append(usernameEnvVar).append("\") != null"); + } + condition.append(")"); + } else { + condition.append("true"); } - condition.append(") && (passwordSupplier != null"); - if (passwordEnvVar != null) { - condition.append(" || System.getenv(\"").append(passwordEnvVar).append("\") != null"); + + condition.append(" && "); + + if (!passwordOmitted) { + builder.addParameter(stringSupplierType, "passwordSupplier"); + condition.append("(passwordSupplier != null"); + if (passwordEnvVar != null) { + condition.append(" || System.getenv(\"").append(passwordEnvVar).append("\") != null"); + } + condition.append(")"); + } else { + condition.append("true"); } - condition.append(")"); builder.addStatement("return " + condition); diff --git a/generators/java/sdk/versions.yml b/generators/java/sdk/versions.yml index aecb7dc6c303..f2a108f329f2 100644 --- a/generators/java/sdk/versions.yml +++ b/generators/java/sdk/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 4.2.3 + changelogEntry: + - summary: | + Support omitting username or password from basic auth when configured via + `usernameOmit` or `passwordOmit` in the IR. Omitted fields are removed from + the SDK's public API and treated as empty strings internally (e.g., omitting + password encodes `username:`, omitting username encodes `:password`). When + both are omitted, the Authorization header is skipped entirely. + type: feat + createdAt: "2026-04-16" + irVersion: 66 + - version: 4.2.2 changelogEntry: - summary: | @@ -35,7 +47,6 @@ type: fix createdAt: "2026-04-14" irVersion: 66 - - version: 4.2.0 changelogEntry: - summary: | @@ -59,7 +70,6 @@ type: feat createdAt: "2026-04-10" irVersion: 66 - - version: 4.2.0-rc.0 changelogEntry: - summary: | @@ -89,7 +99,6 @@ type: fix createdAt: "2026-04-09" irVersion: 65 - - version: 4.1.2 changelogEntry: - summary: | @@ -100,7 +109,6 @@ type: fix createdAt: "2026-04-08" irVersion: 65 - - version: 4.1.1 changelogEntry: - summary: | diff --git a/seed/java-sdk/basic-auth-pw-omitted/snippet.json b/seed/java-sdk/basic-auth-pw-omitted/snippet.json index 0ff774600c61..18255bc819ec 100644 --- a/seed/java-sdk/basic-auth-pw-omitted/snippet.json +++ b/seed/java-sdk/basic-auth-pw-omitted/snippet.json @@ -9,8 +9,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n" } }, { @@ -22,8 +22,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n" } }, { @@ -35,8 +35,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().getWithBasicAuth();\n }\n}\n" } }, { @@ -48,8 +48,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" } }, { @@ -61,8 +61,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" } }, { @@ -74,8 +74,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" } }, { @@ -87,10 +87,10 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\", \"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.basicAuthPwOmitted.SeedBasicAuthPwOmittedClient;\nimport java.util.HashMap;\n\npublic class Example {\n public static void main(String[] args) {\n SeedBasicAuthPwOmittedClient client = SeedBasicAuthPwOmittedClient\n .builder()\n .credentials(\"\")\n .build();\n\n client.basicAuth().postWithBasicAuth(new \n HashMap() {{put(\"key\", \"value\");\n }});\n }\n}\n" } } ], "types": {} -} \ No newline at end of file +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/AsyncSeedBasicAuthPwOmittedClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/AsyncSeedBasicAuthPwOmittedClient.java new file mode 100644 index 000000000000..a86b77ef2b94 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/AsyncSeedBasicAuthPwOmittedClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted; + +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.Suppliers; +import com.seed.basicAuthPwOmitted.resources.basicauth.AsyncBasicAuthClient; +import java.util.function.Supplier; + +public class AsyncSeedBasicAuthPwOmittedClient { + protected final ClientOptions clientOptions; + + protected final Supplier basicAuthClient; + + public AsyncSeedBasicAuthPwOmittedClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.basicAuthClient = Suppliers.memoize(() -> new AsyncBasicAuthClient(clientOptions)); + } + + public AsyncBasicAuthClient basicAuth() { + return this.basicAuthClient.get(); + } + + public static AsyncSeedBasicAuthPwOmittedClientBuilder builder() { + return new AsyncSeedBasicAuthPwOmittedClientBuilder(); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/AsyncSeedBasicAuthPwOmittedClientBuilder.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/AsyncSeedBasicAuthPwOmittedClientBuilder.java new file mode 100644 index 000000000000..bda204d383d3 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/AsyncSeedBasicAuthPwOmittedClientBuilder.java @@ -0,0 +1,224 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted; + +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.Environment; +import com.seed.basicAuthPwOmitted.core.LogConfig; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class AsyncSeedBasicAuthPwOmittedClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String username = null; + + private Environment environment; + + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + + public AsyncSeedBasicAuthPwOmittedClientBuilder credentials(String username) { + this.username = username; + return this; + } + + public AsyncSeedBasicAuthPwOmittedClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public AsyncSeedBasicAuthPwOmittedClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public AsyncSeedBasicAuthPwOmittedClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public AsyncSeedBasicAuthPwOmittedClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public AsyncSeedBasicAuthPwOmittedClientBuilder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public AsyncSeedBasicAuthPwOmittedClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + setLogging(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.username != null) { + String unencodedToken = this.username + ":" + ""; + String encodedToken = Base64.getEncoder().encodeToString(unencodedToken.getBytes()); + builder.addHeader("Authorization", "Basic " + encodedToken); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Sets the logging configuration for the SDK. + * Override this method to customize logging behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setLogging(ClientOptions.Builder builder) { + if (this.logging.isPresent()) { + builder.logging(this.logging.get()); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public AsyncSeedBasicAuthPwOmittedClient build() { + if (this.username == null) { + throw new RuntimeException("Please provide username"); + } + validateConfiguration(); + return new AsyncSeedBasicAuthPwOmittedClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/SeedBasicAuthPwOmittedClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/SeedBasicAuthPwOmittedClient.java new file mode 100644 index 000000000000..60e235c36ffe --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/SeedBasicAuthPwOmittedClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted; + +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.Suppliers; +import com.seed.basicAuthPwOmitted.resources.basicauth.BasicAuthClient; +import java.util.function.Supplier; + +public class SeedBasicAuthPwOmittedClient { + protected final ClientOptions clientOptions; + + protected final Supplier basicAuthClient; + + public SeedBasicAuthPwOmittedClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.basicAuthClient = Suppliers.memoize(() -> new BasicAuthClient(clientOptions)); + } + + public BasicAuthClient basicAuth() { + return this.basicAuthClient.get(); + } + + public static SeedBasicAuthPwOmittedClientBuilder builder() { + return new SeedBasicAuthPwOmittedClientBuilder(); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/SeedBasicAuthPwOmittedClientBuilder.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/SeedBasicAuthPwOmittedClientBuilder.java new file mode 100644 index 000000000000..cb90506676bf --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/SeedBasicAuthPwOmittedClientBuilder.java @@ -0,0 +1,224 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted; + +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.Environment; +import com.seed.basicAuthPwOmitted.core.LogConfig; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class SeedBasicAuthPwOmittedClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String username = null; + + private Environment environment; + + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + + public SeedBasicAuthPwOmittedClientBuilder credentials(String username) { + this.username = username; + return this; + } + + public SeedBasicAuthPwOmittedClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public SeedBasicAuthPwOmittedClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public SeedBasicAuthPwOmittedClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public SeedBasicAuthPwOmittedClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public SeedBasicAuthPwOmittedClientBuilder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public SeedBasicAuthPwOmittedClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + setLogging(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.username != null) { + String unencodedToken = this.username + ":" + ""; + String encodedToken = Base64.getEncoder().encodeToString(unencodedToken.getBytes()); + builder.addHeader("Authorization", "Basic " + encodedToken); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Sets the logging configuration for the SDK. + * Override this method to customize logging behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setLogging(ClientOptions.Builder builder) { + if (this.logging.isPresent()) { + builder.logging(this.logging.get()); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public SeedBasicAuthPwOmittedClient build() { + if (this.username == null) { + throw new RuntimeException("Please provide username"); + } + validateConfiguration(); + return new SeedBasicAuthPwOmittedClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ClientOptions.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ClientOptions.java new file mode 100644 index 000000000000..96c093cea250 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ClientOptions.java @@ -0,0 +1,221 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import okhttp3.OkHttpClient; + +public final class ClientOptions { + private final Environment environment; + + private final Map headers; + + private final Map> headerSuppliers; + + private final OkHttpClient httpClient; + + private final int timeout; + + private final int maxRetries; + + private final Optional logging; + + private ClientOptions( + Environment environment, + Map headers, + Map> headerSuppliers, + OkHttpClient httpClient, + int timeout, + int maxRetries, + Optional logging) { + this.environment = environment; + this.headers = new HashMap<>(); + this.headers.putAll(headers); + this.headers.putAll(new HashMap() { + { + put("User-Agent", "com.fern:basic-auth-pw-omitted/0.0.1"); + put("X-Fern-Language", "JAVA"); + put("X-Fern-SDK-Name", "com.seed.fern:basic-auth-pw-omitted-sdk"); + } + }); + this.headerSuppliers = headerSuppliers; + this.httpClient = httpClient; + this.timeout = timeout; + this.maxRetries = maxRetries; + this.logging = logging; + } + + public Environment environment() { + return this.environment; + } + + public Map headers(RequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public int timeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.timeout; + } + return requestOptions.getTimeout().orElse(this.timeout); + } + + public OkHttpClient httpClient() { + return this.httpClient; + } + + public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public int maxRetries() { + return this.maxRetries; + } + + public Optional logging() { + return this.logging; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Environment environment; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private int maxRetries = 2; + + private Optional timeout = Optional.empty(); + + private OkHttpClient httpClient = null; + + private Optional logging = Optional.empty(); + + public Builder environment(Environment environment) { + this.environment = environment; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(Optional timeout) { + this.timeout = timeout; + return this; + } + + /** + * Override the maximum number of retries. Defaults to 2 retries. + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public Builder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public Builder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + public ClientOptions build() { + OkHttpClient.Builder httpClientBuilder = + this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder(); + + if (this.httpClient != null) { + timeout.ifPresent(timeout -> httpClientBuilder + .callTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS)); + } else { + httpClientBuilder + .callTimeout(this.timeout.orElse(60), TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .addInterceptor(new RetryInterceptor(this.maxRetries)); + } + + Logger logger = Logger.from(this.logging); + httpClientBuilder.addInterceptor(new LoggingInterceptor(logger)); + + this.httpClient = httpClientBuilder.build(); + this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000); + + return new ClientOptions( + environment, + headers, + headerSuppliers, + httpClient, + this.timeout.get(), + this.maxRetries, + this.logging); + } + + /** + * Create a new Builder initialized with values from an existing ClientOptions + */ + public static Builder from(ClientOptions clientOptions) { + Builder builder = new Builder(); + builder.environment = clientOptions.environment(); + builder.timeout = Optional.of(clientOptions.timeout(null)); + builder.httpClient = clientOptions.httpClient(); + builder.headers.putAll(clientOptions.headers); + builder.headerSuppliers.putAll(clientOptions.headerSuppliers); + builder.maxRetries = clientOptions.maxRetries(); + builder.logging = clientOptions.logging(); + return builder; + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ConsoleLogger.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ConsoleLogger.java new file mode 100644 index 000000000000..91a7a2e7b733 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ConsoleLogger.java @@ -0,0 +1,51 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.logging.Level; + +/** + * Default logger implementation that writes to the console using {@link java.util.logging.Logger}. + * + *

Uses the "fern" logger name with a simple format of "LEVEL - message". + */ +public final class ConsoleLogger implements ILogger { + + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("fern"); + + static { + if (logger.getHandlers().length == 0) { + java.util.logging.ConsoleHandler handler = new java.util.logging.ConsoleHandler(); + handler.setFormatter(new java.util.logging.SimpleFormatter() { + @Override + public String format(java.util.logging.LogRecord record) { + return record.getLevel() + " - " + record.getMessage() + System.lineSeparator(); + } + }); + logger.addHandler(handler); + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + } + } + + @Override + public void debug(String message) { + logger.log(Level.FINE, message); + } + + @Override + public void info(String message) { + logger.log(Level.INFO, message); + } + + @Override + public void warn(String message) { + logger.log(Level.WARNING, message); + } + + @Override + public void error(String message) { + logger.log(Level.SEVERE, message); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/DateTimeDeserializer.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/DateTimeDeserializer.java new file mode 100644 index 000000000000..12c827d9dac8 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/DateTimeDeserializer.java @@ -0,0 +1,55 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/DoubleSerializer.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/DoubleSerializer.java new file mode 100644 index 000000000000..2883eb926e01 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/DoubleSerializer.java @@ -0,0 +1,43 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; + +/** + * Custom serializer that writes integer-valued doubles without a decimal point. + * For example, {@code 24000.0} is serialized as {@code 24000} instead of {@code 24000.0}. + * Non-integer values like {@code 3.14} are serialized normally. + */ +class DoubleSerializer extends JsonSerializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule() + .addSerializer(Double.class, new DoubleSerializer()) + .addSerializer(double.class, new DoubleSerializer()); + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null && value == Math.floor(value) && !Double.isInfinite(value) && !Double.isNaN(value)) { + gen.writeNumber(value.longValue()); + } else { + gen.writeNumber(value); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Environment.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Environment.java new file mode 100644 index 000000000000..8b3871d0c7d7 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Environment.java @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +public final class Environment { + private final String url; + + private Environment(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + + public static Environment custom(String url) { + return new Environment(url); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/FileStream.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/FileStream.java new file mode 100644 index 000000000000..9e82fd9993d3 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/FileStream.java @@ -0,0 +1,60 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a file stream with associated metadata for file uploads. + */ +public class FileStream { + private final InputStream inputStream; + private final String fileName; + private final MediaType contentType; + + /** + * Constructs a FileStream with the given input stream and optional metadata. + * + * @param inputStream The input stream of the file content. Must not be null. + * @param fileName The name of the file, or null if unknown. + * @param contentType The MIME type of the file content, or null if unknown. + * @throws NullPointerException if inputStream is null + */ + public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) { + this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null"); + this.fileName = fileName; + this.contentType = contentType; + } + + public FileStream(InputStream inputStream) { + this(inputStream, null, null); + } + + public InputStream getInputStream() { + return inputStream; + } + + @Nullable + public String getFileName() { + return fileName; + } + + @Nullable + public MediaType getContentType() { + return contentType; + } + + /** + * Creates a RequestBody suitable for use with OkHttp client. + * + * @return A RequestBody instance representing this file stream. + */ + public RequestBody toRequestBody() { + return new InputStreamRequestBody(contentType, inputStream); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ILogger.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ILogger.java new file mode 100644 index 000000000000..5fb2c2e17977 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ILogger.java @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +/** + * Interface for custom logger implementations. + * + *

Implement this interface to provide a custom logging backend for the SDK. + * The SDK will call the appropriate method based on the log level. + * + *

Example: + *

{@code
+ * public class MyCustomLogger implements ILogger {
+ *     public void debug(String message) {
+ *         System.out.println("[DBG] " + message);
+ *     }
+ *     public void info(String message) {
+ *         System.out.println("[INF] " + message);
+ *     }
+ *     public void warn(String message) {
+ *         System.out.println("[WRN] " + message);
+ *     }
+ *     public void error(String message) {
+ *         System.out.println("[ERR] " + message);
+ *     }
+ * }
+ * }
+ */ +public interface ILogger { + void debug(String message); + + void info(String message); + + void warn(String message); + + void error(String message); +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/InputStreamRequestBody.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/InputStreamRequestBody.java new file mode 100644 index 000000000000..7f50828e29cb --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/InputStreamRequestBody.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.jetbrains.annotations.Nullable; + +/** + * A custom implementation of OkHttp's RequestBody that wraps an InputStream. + * This class allows streaming of data from an InputStream directly to an HTTP request body, + * which is useful for file uploads or sending large amounts of data without loading it all into memory. + */ +public class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType contentType; + + /** + * Constructs an InputStreamRequestBody with the specified content type and input stream. + * + * @param contentType the MediaType of the content, or null if not known + * @param inputStream the InputStream containing the data to be sent + * @throws NullPointerException if inputStream is null + */ + public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null"); + } + + /** + * Returns the content type of this request body. + * + * @return the MediaType of the content, or null if not specified + */ + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + /** + * Returns the content length of this request body, if known. + * This method attempts to determine the length using the InputStream's available() method, + * which may not always accurately reflect the total length of the stream. + * + * @return the content length, or -1 if the length is unknown + * @throws IOException if an I/O error occurs + */ + @Override + public long contentLength() throws IOException { + return inputStream.available() == 0 ? -1 : inputStream.available(); + } + + /** + * Writes the content of the InputStream to the given BufferedSink. + * This method is responsible for transferring the data from the InputStream to the network request. + * + * @param sink the BufferedSink to write the content to + * @throws IOException if an I/O error occurs during writing + */ + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (Source source = Okio.source(inputStream)) { + sink.writeAll(source); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LogConfig.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LogConfig.java new file mode 100644 index 000000000000..823cfd80fe16 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LogConfig.java @@ -0,0 +1,98 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +/** + * Configuration for SDK logging. + * + *

Use the builder to configure logging behavior: + *

{@code
+ * LogConfig config = LogConfig.builder()
+ *     .level(LogLevel.DEBUG)
+ *     .silent(false)
+ *     .build();
+ * }
+ * + *

Or with a custom logger: + *

{@code
+ * LogConfig config = LogConfig.builder()
+ *     .level(LogLevel.DEBUG)
+ *     .logger(new MyCustomLogger())
+ *     .silent(false)
+ *     .build();
+ * }
+ * + *

Defaults: + *

    + *
  • {@code level} — {@link LogLevel#INFO}
  • + *
  • {@code logger} — {@link ConsoleLogger} (writes to stderr via java.util.logging)
  • + *
  • {@code silent} — {@code true} (no output unless explicitly enabled)
  • + *
+ */ +public final class LogConfig { + + private final LogLevel level; + private final ILogger logger; + private final boolean silent; + + private LogConfig(LogLevel level, ILogger logger, boolean silent) { + this.level = level; + this.logger = logger; + this.silent = silent; + } + + public LogLevel level() { + return level; + } + + public ILogger logger() { + return logger; + } + + public boolean silent() { + return silent; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private LogLevel level = LogLevel.INFO; + private ILogger logger = new ConsoleLogger(); + private boolean silent = true; + + private Builder() {} + + /** + * Set the minimum log level. Only messages at this level or above will be logged. + * Defaults to {@link LogLevel#INFO}. + */ + public Builder level(LogLevel level) { + this.level = level; + return this; + } + + /** + * Set a custom logger implementation. Defaults to {@link ConsoleLogger}. + */ + public Builder logger(ILogger logger) { + this.logger = logger; + return this; + } + + /** + * Set whether logging is silent (disabled). Defaults to {@code true}. + * Set to {@code false} to enable log output. + */ + public Builder silent(boolean silent) { + this.silent = silent; + return this; + } + + public LogConfig build() { + return new LogConfig(level, logger, silent); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LogLevel.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LogLevel.java new file mode 100644 index 000000000000..5da5885e2e9e --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LogLevel.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +/** + * Log levels for SDK logging configuration. + * Silent by default — no log output unless explicitly configured. + */ +public enum LogLevel { + DEBUG(1), + INFO(2), + WARN(3), + ERROR(4); + + private final int value; + + LogLevel(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + /** + * Parse a log level from a string (case-insensitive). + * + * @param level the level string (debug, info, warn, error) + * @return the corresponding LogLevel + * @throws IllegalArgumentException if the string does not match any level + */ + public static LogLevel fromString(String level) { + return LogLevel.valueOf(level.toUpperCase()); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Logger.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Logger.java new file mode 100644 index 000000000000..dde944d415b7 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Logger.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +/** + * SDK logger that filters messages based on level and silent mode. + * + *

Silent by default — no log output unless explicitly configured. + * Create via {@link LogConfig} or directly: + *

{@code
+ * Logger logger = new Logger(LogLevel.DEBUG, new ConsoleLogger(), false);
+ * logger.debug("request sent");
+ * }
+ */ +public final class Logger { + + private static final Logger DEFAULT = new Logger(LogLevel.INFO, new ConsoleLogger(), true); + + private final LogLevel level; + private final ILogger logger; + private final boolean silent; + + public Logger(LogLevel level, ILogger logger, boolean silent) { + this.level = level; + this.logger = logger; + this.silent = silent; + } + + /** + * Returns a default silent logger (no output). + */ + public static Logger getDefault() { + return DEFAULT; + } + + /** + * Creates a Logger from a {@link LogConfig}. If config is {@code null}, returns the default silent logger. + */ + public static Logger from(LogConfig config) { + if (config == null) { + return DEFAULT; + } + return new Logger(config.level(), config.logger(), config.silent()); + } + + /** + * Creates a Logger from an {@code Optional}. If empty, returns the default silent logger. + */ + public static Logger from(java.util.Optional config) { + return config.map(Logger::from).orElse(DEFAULT); + } + + private boolean shouldLog(LogLevel messageLevel) { + return !silent && level.getValue() <= messageLevel.getValue(); + } + + public boolean isDebug() { + return shouldLog(LogLevel.DEBUG); + } + + public boolean isInfo() { + return shouldLog(LogLevel.INFO); + } + + public boolean isWarn() { + return shouldLog(LogLevel.WARN); + } + + public boolean isError() { + return shouldLog(LogLevel.ERROR); + } + + public void debug(String message) { + if (isDebug()) { + logger.debug(message); + } + } + + public void info(String message) { + if (isInfo()) { + logger.info(message); + } + } + + public void warn(String message) { + if (isWarn()) { + logger.warn(message); + } + } + + public void error(String message) { + if (isError()) { + logger.error(message); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LoggingInterceptor.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LoggingInterceptor.java new file mode 100644 index 000000000000..8a6184283f93 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/LoggingInterceptor.java @@ -0,0 +1,104 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * OkHttp interceptor that logs HTTP requests and responses. + * + *

Logs request method, URL, and headers (with sensitive values redacted) at debug level. + * Logs response status at debug level, and 4xx/5xx responses at error level. + * Does nothing if the logger is silent. + */ +public final class LoggingInterceptor implements Interceptor { + + private static final Set SENSITIVE_HEADERS = new HashSet<>(Arrays.asList( + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "proxy-authenticate", + "proxy-authorization", + "cookie", + "set-cookie", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token")); + + private final Logger logger; + + public LoggingInterceptor(Logger logger) { + this.logger = logger; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + if (logger.isDebug()) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP Request: ").append(request.method()).append(" ").append(request.url()); + sb.append(" headers={"); + boolean first = true; + for (String name : request.headers().names()) { + if (!first) { + sb.append(", "); + } + sb.append(name).append("="); + if (SENSITIVE_HEADERS.contains(name.toLowerCase())) { + sb.append("[REDACTED]"); + } else { + sb.append(request.header(name)); + } + first = false; + } + sb.append("}"); + sb.append(" has_body=").append(request.body() != null); + logger.debug(sb.toString()); + } + + Response response = chain.proceed(request); + + if (logger.isDebug()) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP Response: status=").append(response.code()); + sb.append(" url=").append(response.request().url()); + sb.append(" headers={"); + boolean first = true; + for (String name : response.headers().names()) { + if (!first) { + sb.append(", "); + } + sb.append(name).append("="); + if (SENSITIVE_HEADERS.contains(name.toLowerCase())) { + sb.append("[REDACTED]"); + } else { + sb.append(response.header(name)); + } + first = false; + } + sb.append("}"); + logger.debug(sb.toString()); + } + + if (response.code() >= 400 && logger.isError()) { + logger.error("HTTP Error: status=" + response.code() + " url=" + + response.request().url()); + } + + return response; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/MediaTypes.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/MediaTypes.java new file mode 100644 index 000000000000..39f2bdc9368c --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/MediaTypes.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import okhttp3.MediaType; + +public final class MediaTypes { + + public static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + private MediaTypes() {} +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Nullable.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Nullable.java new file mode 100644 index 000000000000..7b39b4f437b1 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Nullable.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.Optional; +import java.util.function.Function; + +public final class Nullable { + + private final Either, Null> value; + + private Nullable() { + this.value = Either.left(Optional.empty()); + } + + private Nullable(T value) { + if (value == null) { + this.value = Either.right(Null.INSTANCE); + } else { + this.value = Either.left(Optional.of(value)); + } + } + + public static Nullable ofNull() { + return new Nullable<>(null); + } + + public static Nullable of(T value) { + return new Nullable<>(value); + } + + public static Nullable empty() { + return new Nullable<>(); + } + + public static Nullable ofOptional(Optional value) { + if (value.isPresent()) { + return of(value.get()); + } else { + return empty(); + } + } + + public boolean isNull() { + return this.value.isRight(); + } + + public boolean isEmpty() { + return this.value.isLeft() && !this.value.getLeft().isPresent(); + } + + public T get() { + if (this.isNull()) { + return null; + } + + return this.value.getLeft().get(); + } + + public Nullable map(Function mapper) { + if (this.isNull()) { + return Nullable.ofNull(); + } + + return Nullable.ofOptional(this.value.getLeft().map(mapper)); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Nullable)) { + return false; + } + + if (((Nullable) other).isNull() && this.isNull()) { + return true; + } + + return this.value.getLeft().equals(((Nullable) other).value.getLeft()); + } + + private static final class Either { + private L left = null; + private R right = null; + + private Either(L left, R right) { + if (left != null && right != null) { + throw new IllegalArgumentException("Left and right argument cannot both be non-null."); + } + + if (left == null && right == null) { + throw new IllegalArgumentException("Left and right argument cannot both be null."); + } + + if (left != null) { + this.left = left; + } + + if (right != null) { + this.right = right; + } + } + + public static Either left(L left) { + return new Either<>(left, null); + } + + public static Either right(R right) { + return new Either<>(null, right); + } + + public boolean isLeft() { + return this.left != null; + } + + public boolean isRight() { + return this.right != null; + } + + public L getLeft() { + if (!this.isLeft()) { + throw new IllegalArgumentException("Cannot get left from right Either."); + } + return this.left; + } + + public R getRight() { + if (!this.isRight()) { + throw new IllegalArgumentException("Cannot get right from left Either."); + } + return this.right; + } + } + + private static final class Null { + private static final Null INSTANCE = new Null(); + + private Null() {} + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/NullableNonemptyFilter.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/NullableNonemptyFilter.java new file mode 100644 index 000000000000..a2a10bf6a59b --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/NullableNonemptyFilter.java @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.Optional; + +public final class NullableNonemptyFilter { + @Override + public boolean equals(Object o) { + boolean isOptionalEmpty = isOptionalEmpty(o); + + return isOptionalEmpty; + } + + private boolean isOptionalEmpty(Object o) { + if (o instanceof Optional) { + return !((Optional) o).isPresent(); + } + return false; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ObjectMappers.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ObjectMappers.java new file mode 100644 index 000000000000..efc5711cfc13 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ObjectMappers.java @@ -0,0 +1,46 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .addModule(DoubleSerializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } + + public static Object parseErrorBody(String responseBodyString) { + try { + return JSON_MAPPER.readValue(responseBodyString, Object.class); + } catch (JsonProcessingException ignored) { + return responseBodyString; + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/QueryStringMapper.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/QueryStringMapper.java new file mode 100644 index 000000000000..7e2cdb97584e --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/QueryStringMapper.java @@ -0,0 +1,142 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import okhttp3.MultipartBody; + +public class QueryStringMapper { + + private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER; + + public static void addQueryParameter(HttpUrl.Builder httpUrl, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + httpUrl.addQueryParameter(key, valueNode.textValue()); + } else { + httpUrl.addQueryParameter(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().textValue()); + } else { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().toString()); + } + } + } + + public static void addFormDataPart( + MultipartBody.Builder multipartBody, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + multipartBody.addFormDataPart(key, valueNode.textValue()); + } else { + multipartBody.addFormDataPart(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().textValue()); + } else { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().toString()); + } + } + } + + public static List> flattenObject(ObjectNode object, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator> fields = object.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + + String key = "[" + field.getKey() + "]"; + + if (field.getValue().isObject()) { + List> flatField = + flattenObject((ObjectNode) field.getValue(), arraysAsRepeats); + addAll(flat, flatField, key); + } else if (field.getValue().isArray()) { + List> flatField = + flattenArray((ArrayNode) field.getValue(), key, arraysAsRepeats); + addAll(flat, flatField, ""); + } else { + flat.add(new AbstractMap.SimpleEntry<>(key, field.getValue())); + } + } + + return flat; + } + + private static List> flattenArray( + ArrayNode array, String key, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator elements = array.elements(); + + int index = 0; + while (elements.hasNext()) { + JsonNode element = elements.next(); + + String indexKey = key + "[" + index + "]"; + + if (arraysAsRepeats) { + indexKey = key; + } + + if (element.isObject()) { + List> flatField = flattenObject((ObjectNode) element, arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else if (element.isArray()) { + List> flatField = flattenArray((ArrayNode) element, "", arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else { + flat.add(new AbstractMap.SimpleEntry<>(indexKey, element)); + } + + index++; + } + + return flat; + } + + private static void addAll( + List> target, List> source, String prefix) { + for (Map.Entry entry : source) { + Map.Entry entryToAdd = + new AbstractMap.SimpleEntry<>(prefix + entry.getKey(), entry.getValue()); + target.add(entryToAdd); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/RequestOptions.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/RequestOptions.java new file mode 100644 index 000000000000..ac82a2af0d0b --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/RequestOptions.java @@ -0,0 +1,118 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class RequestOptions { + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Map headers; + + private final Map> headerSuppliers; + + private final Map queryParameters; + + private final Map> queryParameterSuppliers; + + private RequestOptions( + Optional timeout, + TimeUnit timeoutTimeUnit, + Map headers, + Map> headerSuppliers, + Map queryParameters, + Map> queryParameterSuppliers) { + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + this.queryParameters = queryParameters; + this.queryParameterSuppliers = queryParameterSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public Map getQueryParameters() { + Map queryParameters = new HashMap<>(this.queryParameters); + this.queryParameterSuppliers.forEach((key, supplier) -> { + queryParameters.put(key, supplier.get()); + }); + return queryParameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private final Map queryParameters = new HashMap<>(); + + private final Map> queryParameterSuppliers = new HashMap<>(); + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, String value) { + this.queryParameters.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, Supplier value) { + this.queryParameterSuppliers.put(key, value); + return this; + } + + public RequestOptions build() { + return new RequestOptions( + timeout, timeoutTimeUnit, headers, headerSuppliers, queryParameters, queryParameterSuppliers); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ResponseBodyInputStream.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ResponseBodyInputStream.java new file mode 100644 index 000000000000..992d908f2f23 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ResponseBodyInputStream.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.FilterInputStream; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the stream is closed. + * + * This class extends FilterInputStream and takes an OkHttp Response object as a parameter. + * It retrieves the InputStream from the Response and overrides the close method to close + * both the InputStream and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyInputStream extends FilterInputStream { + private final Response response; + + /** + * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp + * Response object. + * + * @param response the OkHttp Response object from which the InputStream is retrieved + * @throws IOException if an I/O error occurs while retrieving the InputStream + */ + public ResponseBodyInputStream(Response response) throws IOException { + super(response.body().byteStream()); + this.response = response; + } + + /** + * Closes the InputStream and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the stream is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the stream is closed + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ResponseBodyReader.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ResponseBodyReader.java new file mode 100644 index 000000000000..aadf0b06004b --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/ResponseBodyReader.java @@ -0,0 +1,44 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.FilterReader; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the reader is closed. + * + * This class extends FilterReader and takes an OkHttp Response object as a parameter. + * It retrieves the Reader from the Response and overrides the close method to close + * both the Reader and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyReader extends FilterReader { + private final Response response; + + /** + * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object. + * + * @param response the OkHttp Response object from which the Reader is retrieved + * @throws IOException if an I/O error occurs while retrieving the Reader + */ + public ResponseBodyReader(Response response) throws IOException { + super(response.body().charStream()); + this.response = response; + } + + /** + * Closes the Reader and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the reader is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the reader is closed + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/RetryInterceptor.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/RetryInterceptor.java new file mode 100644 index 000000000000..7ebba950b745 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/RetryInterceptor.java @@ -0,0 +1,181 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.Random; +import okhttp3.Interceptor; +import okhttp3.Response; + +public class RetryInterceptor implements Interceptor { + + private static final Duration INITIAL_RETRY_DELAY = Duration.ofMillis(1000); + private static final Duration MAX_RETRY_DELAY = Duration.ofMillis(60000); + private static final double JITTER_FACTOR = 0.2; + + private final int maxRetries; + private final Random random = new Random(); + + public RetryInterceptor(int maxRetries) { + this.maxRetries = maxRetries; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + if (shouldRetry(response.code())) { + return retryChain(response, chain); + } + + return response; + } + + private Response retryChain(Response response, Chain chain) throws IOException { + ExponentialBackoff backoff = new ExponentialBackoff(this.maxRetries); + Optional nextBackoff = backoff.nextBackoff(response); + while (nextBackoff.isPresent()) { + try { + Thread.sleep(nextBackoff.get().toMillis()); + } catch (InterruptedException e) { + throw new IOException("Interrupted while trying request", e); + } + response.close(); + response = chain.proceed(chain.request()); + if (shouldRetry(response.code())) { + nextBackoff = backoff.nextBackoff(response); + } else { + return response; + } + } + + return response; + } + + /** + * Calculates the retry delay from response headers, with fallback to exponential backoff. + * Priority: Retry-After > X-RateLimit-Reset > Exponential Backoff + */ + private Duration getRetryDelayFromHeaders(Response response, int retryAttempt) { + // Check for Retry-After header first (RFC 7231), with no jitter + String retryAfter = response.header("Retry-After"); + if (retryAfter != null) { + // Parse as number of seconds... + Optional secondsDelay = tryParseLong(retryAfter) + .map(seconds -> seconds * 1000) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, MAX_RETRY_DELAY.toMillis())) + .map(Duration::ofMillis); + if (secondsDelay.isPresent()) { + return secondsDelay.get(); + } + + // ...or as an HTTP date; both are valid + Optional dateDelay = tryParseHttpDate(retryAfter) + .map(resetTime -> resetTime.toInstant().toEpochMilli() - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, MAX_RETRY_DELAY.toMillis())) + .map(Duration::ofMillis); + if (dateDelay.isPresent()) { + return dateDelay.get(); + } + } + + // Then check for industry-standard X-RateLimit-Reset header, with positive jitter + String rateLimitReset = response.header("X-RateLimit-Reset"); + if (rateLimitReset != null) { + // Assume Unix timestamp in epoch seconds + Optional rateLimitDelay = tryParseLong(rateLimitReset) + .map(resetTimeSeconds -> (resetTimeSeconds * 1000) - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, MAX_RETRY_DELAY.toMillis())) + .map(this::addPositiveJitter) + .map(Duration::ofMillis); + if (rateLimitDelay.isPresent()) { + return rateLimitDelay.get(); + } + } + + // Fall back to exponential backoff, with symmetric jitter + long baseDelay = INITIAL_RETRY_DELAY.toMillis() * (1L << retryAttempt); // 2^retryAttempt + long cappedDelay = Math.min(baseDelay, MAX_RETRY_DELAY.toMillis()); + return Duration.ofMillis(addSymmetricJitter(cappedDelay)); + } + + /** + * Attempts to parse a string as a long, returning empty Optional on failure. + */ + private Optional tryParseLong(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(Long.parseLong(value)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + /** + * Attempts to parse a string as an HTTP date (RFC 1123), returning empty Optional on failure. + */ + private Optional tryParseHttpDate(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + /** + * Adds positive jitter (100-120% of original value) to prevent thundering herd. + * Used for X-RateLimit-Reset header delays. + */ + private long addPositiveJitter(long delayMs) { + double jitterMultiplier = 1.0 + (random.nextDouble() * JITTER_FACTOR); + return (long) (delayMs * jitterMultiplier); + } + + /** + * Adds symmetric jitter (90-110% of original value) to prevent thundering herd. + * Used for exponential backoff delays. + */ + private long addSymmetricJitter(long delayMs) { + double jitterMultiplier = 1.0 + ((random.nextDouble() - 0.5) * JITTER_FACTOR); + return (long) (delayMs * jitterMultiplier); + } + + private static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 429 || statusCode >= 500; + } + + private final class ExponentialBackoff { + + private final int maxNumRetries; + + private int retryNumber = 0; + + ExponentialBackoff(int maxNumRetries) { + this.maxNumRetries = maxNumRetries; + } + + public Optional nextBackoff(Response response) { + if (retryNumber >= maxNumRetries) { + return Optional.empty(); + } + + Duration delay = getRetryDelayFromHeaders(response, retryNumber); + retryNumber += 1; + return Optional.of(delay); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Rfc2822DateTimeDeserializer.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Rfc2822DateTimeDeserializer.java new file mode 100644 index 000000000000..8944f7d2daf7 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Rfc2822DateTimeDeserializer.java @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Custom deserializer that handles converting RFC 2822 (RFC 1123) dates into {@link OffsetDateTime} objects. + * This is used for fields with format "date-time-rfc-2822", such as Twilio's dateCreated, dateSent, dateUpdated. + */ +public class Rfc2822DateTimeDeserializer extends JsonDeserializer { + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + String raw = parser.getValueAsString(); + return ZonedDateTime.parse(raw, DateTimeFormatter.RFC_1123_DATE_TIME).toOffsetDateTime(); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedApiException.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedApiException.java new file mode 100644 index 000000000000..61178caad7f3 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedApiException.java @@ -0,0 +1,73 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +/** + * This exception type will be thrown for any non-2XX API responses. + */ +public class SeedBasicAuthPwOmittedApiException extends SeedBasicAuthPwOmittedException { + /** + * The error code of the response that triggered the exception. + */ + private final int statusCode; + + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + private final Map> headers; + + public SeedBasicAuthPwOmittedApiException(String message, int statusCode, Object body) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + } + + public SeedBasicAuthPwOmittedApiException(String message, int statusCode, Object body, Response rawResponse) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + this.headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + } + + /** + * @return the statusCode + */ + public int statusCode() { + return this.statusCode; + } + + /** + * @return the body + */ + public Object body() { + return this.body; + } + + /** + * @return the headers + */ + public Map> headers() { + return this.headers; + } + + @Override + public String toString() { + return "SeedBasicAuthPwOmittedApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + + ", body: " + ObjectMappers.stringify(body) + "}"; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedException.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedException.java new file mode 100644 index 000000000000..b91ff5e43e1e --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedException.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +/** + * This class serves as the base exception for all errors in the SDK. + */ +public class SeedBasicAuthPwOmittedException extends RuntimeException { + public SeedBasicAuthPwOmittedException(String message) { + super(message); + } + + public SeedBasicAuthPwOmittedException(String message, Exception e) { + super(message, e); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedHttpResponse.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedHttpResponse.java new file mode 100644 index 000000000000..632fe26cc2f6 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SeedBasicAuthPwOmittedHttpResponse.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +public final class SeedBasicAuthPwOmittedHttpResponse { + + private final T body; + + private final Map> headers; + + public SeedBasicAuthPwOmittedHttpResponse(T body, Response rawResponse) { + this.body = body; + + Map> headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + this.headers = headers; + } + + public T body() { + return this.body; + } + + public Map> headers() { + return headers; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SseEvent.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SseEvent.java new file mode 100644 index 000000000000..218b71f93fb2 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SseEvent.java @@ -0,0 +1,114 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a Server-Sent Event with all standard fields. + * Used for event-level discrimination where the discriminator is at the SSE envelope level. + * + * @param The type of the data field + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SseEvent { + private final String event; + private final T data; + private final String id; + private final Long retry; + + private SseEvent(String event, T data, String id, Long retry) { + this.event = event; + this.data = data; + this.id = id; + this.retry = retry; + } + + @JsonProperty("event") + public Optional getEvent() { + return Optional.ofNullable(event); + } + + @JsonProperty("data") + public T getData() { + return data; + } + + @JsonProperty("id") + public Optional getId() { + return Optional.ofNullable(id); + } + + @JsonProperty("retry") + public Optional getRetry() { + return Optional.ofNullable(retry); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SseEvent sseEvent = (SseEvent) o; + return Objects.equals(event, sseEvent.event) + && Objects.equals(data, sseEvent.data) + && Objects.equals(id, sseEvent.id) + && Objects.equals(retry, sseEvent.retry); + } + + @Override + public int hashCode() { + return Objects.hash(event, data, id, retry); + } + + @Override + public String toString() { + return "SseEvent{" + "event='" + + event + '\'' + ", data=" + + data + ", id='" + + id + '\'' + ", retry=" + + retry + '}'; + } + + public static Builder builder() { + return new Builder<>(); + } + + public static final class Builder { + private String event; + private T data; + private String id; + private Long retry; + + private Builder() {} + + public Builder event(String event) { + this.event = event; + return this; + } + + public Builder data(T data) { + this.data = data; + return this; + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder retry(Long retry) { + this.retry = retry; + return this; + } + + public SseEvent build() { + return new SseEvent<>(event, data, id, retry); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SseEventParser.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SseEventParser.java new file mode 100644 index 000000000000..854f57f57396 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/SseEventParser.java @@ -0,0 +1,228 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.type.TypeReference; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Utility class for parsing Server-Sent Events with support for discriminated unions. + *

+ * Handles two discrimination patterns: + *

    + *
  1. Data-level discrimination: The discriminator (e.g., 'type') is inside the JSON data payload. + * Jackson's polymorphic deserialization handles this automatically.
  2. + *
  3. Event-level discrimination: The discriminator (e.g., 'event') is at the SSE envelope level. + * This requires constructing the full SSE envelope for Jackson to process.
  4. + *
+ */ +public final class SseEventParser { + + private static final Set SSE_ENVELOPE_FIELDS = new HashSet<>(Arrays.asList("event", "data", "id", "retry")); + + private SseEventParser() { + // Utility class + } + + /** + * Parse an SSE event using event-level discrimination. + *

+ * Constructs the full SSE envelope object with event, data, id, and retry fields, + * then deserializes it to the target union type. + * + * @param eventType The SSE event type (from event: field) + * @param data The SSE data content (from data: field) + * @param id The SSE event ID (from id: field), may be null + * @param retry The SSE retry value (from retry: field), may be null + * @param unionClass The target union class + * @param discriminatorProperty The property name used for discrimination (e.g., "event") + * @param The target type + * @return The deserialized object + */ + public static T parseEventLevelUnion( + String eventType, String data, String id, Long retry, Class unionClass, String discriminatorProperty) { + try { + // Determine if data should be parsed as JSON based on the variant's expected type + Object parsedData = parseDataForVariant(eventType, data, unionClass, discriminatorProperty); + + // Construct the SSE envelope object + Map envelope = new HashMap<>(); + envelope.put(discriminatorProperty, eventType); + envelope.put("data", parsedData); + if (id != null) { + envelope.put("id", id); + } + if (retry != null) { + envelope.put("retry", retry); + } + + // Serialize to JSON and deserialize to target type + String envelopeJson = ObjectMappers.JSON_MAPPER.writeValueAsString(envelope); + return ObjectMappers.JSON_MAPPER.readValue(envelopeJson, unionClass); + } catch (Exception e) { + throw new RuntimeException("Failed to parse SSE event with event-level discrimination", e); + } + } + + /** + * Parse an SSE event using data-level discrimination. + *

+ * Simply parses the data field as JSON and deserializes it to the target type. + * Jackson's polymorphic deserialization handles the discrimination automatically. + * + * @param data The SSE data content (from data: field) + * @param valueType The target type + * @param The target type + * @return The deserialized object + */ + public static T parseDataLevelUnion(String data, Class valueType) { + try { + return ObjectMappers.JSON_MAPPER.readValue(data, valueType); + } catch (Exception e) { + throw new RuntimeException("Failed to parse SSE data with data-level discrimination", e); + } + } + + /** + * Determines if the given discriminator property indicates event-level discrimination. + * Event-level discrimination occurs when the discriminator is an SSE envelope field. + * + * @param discriminatorProperty The discriminator property name + * @return true if event-level discrimination, false otherwise + */ + public static boolean isEventLevelDiscrimination(String discriminatorProperty) { + return SSE_ENVELOPE_FIELDS.contains(discriminatorProperty); + } + + /** + * Attempts to find the discriminator property from the union class's Jackson annotations. + * + * @param unionClass The union class to inspect + * @return The discriminator property name, or empty if not found + */ + public static Optional findDiscriminatorProperty(Class unionClass) { + try { + // Look for JsonTypeInfo on the class itself + JsonTypeInfo typeInfo = unionClass.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && !typeInfo.property().isEmpty()) { + return Optional.of(typeInfo.property()); + } + + // Look for inner Value interface with JsonTypeInfo + for (Class innerClass : unionClass.getDeclaredClasses()) { + typeInfo = innerClass.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && !typeInfo.property().isEmpty()) { + return Optional.of(typeInfo.property()); + } + } + } catch (Exception e) { + // Ignore reflection errors + } + return Optional.empty(); + } + + /** + * Parse the data field based on what the matching variant expects. + * If the variant expects a String for its data field, returns the raw string. + * Otherwise, parses the data as JSON. + */ + private static Object parseDataForVariant( + String eventType, String data, Class unionClass, String discriminatorProperty) { + if (data == null || data.isEmpty()) { + return data; + } + + try { + // Try to find the variant class that matches this event type + Class variantClass = findVariantClass(unionClass, eventType, discriminatorProperty); + if (variantClass != null) { + // Check if the variant expects a String for the data field + Field dataField = findField(variantClass, "data"); + if (dataField != null && String.class.equals(dataField.getType())) { + // Variant expects String - return raw data + return data; + } + } + + // Try to parse as JSON + return ObjectMappers.JSON_MAPPER.readValue(data, new TypeReference>() {}); + } catch (Exception e) { + // If JSON parsing fails, return as string + return data; + } + } + + /** + * Find the variant class that matches the given discriminator value. + */ + private static Class findVariantClass( + Class unionClass, String discriminatorValue, String discriminatorProperty) { + try { + // Look for JsonSubTypes annotation + JsonSubTypes subTypes = findJsonSubTypes(unionClass); + if (subTypes == null) { + return null; + } + + for (JsonSubTypes.Type subType : subTypes.value()) { + JsonTypeName typeName = subType.value().getAnnotation(JsonTypeName.class); + if (typeName != null && typeName.value().equals(discriminatorValue)) { + return subType.value(); + } + // Also check the name attribute of @JsonSubTypes.Type + if (subType.name().equals(discriminatorValue)) { + return subType.value(); + } + } + } catch (Exception e) { + // Ignore reflection errors + } + return null; + } + + /** + * Find JsonSubTypes annotation on the class or its inner classes. + */ + private static JsonSubTypes findJsonSubTypes(Class unionClass) { + // Check the class itself + JsonSubTypes subTypes = unionClass.getAnnotation(JsonSubTypes.class); + if (subTypes != null) { + return subTypes; + } + + // Check inner classes (for Fern-style unions with inner Value interface) + for (Class innerClass : unionClass.getDeclaredClasses()) { + subTypes = innerClass.getAnnotation(JsonSubTypes.class); + if (subTypes != null) { + return subTypes; + } + } + return null; + } + + /** + * Find a field by name in a class, including private fields. + */ + private static Field findField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + // Check superclass + Class superClass = clazz.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return findField(superClass, fieldName); + } + return null; + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Stream.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Stream.java new file mode 100644 index 000000000000..2b498e4cc012 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Stream.java @@ -0,0 +1,513 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +/** + * The {@code Stream} class implements {@link Iterable} to provide a simple mechanism for reading and parsing + * objects of a given type from data streamed via a {@link Reader} using a specified delimiter. + *

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * Iterable stream for parsing JSON and Server-Sent Events (SSE) data. + * Supports both newline-delimited JSON and SSE with optional stream termination. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable, Closeable { + + private static final String NEWLINE = "\n"; + private static final String DATA_PREFIX = "data:"; + + public enum StreamType { + JSON, + SSE, + SSE_EVENT_DISCRIMINATED + } + + private final Class valueType; + private final Scanner scanner; + private final StreamType streamType; + private final String messageTerminator; + private final String streamTerminator; + private final Reader sseReader; + private final String discriminatorProperty; + private boolean isClosed = false; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.valueType = valueType; + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.streamType = StreamType.JSON; + this.messageTerminator = delimiter; + this.streamTerminator = null; + this.sseReader = null; + this.discriminatorProperty = null; + } + + private Stream(Class valueType, StreamType type, Reader reader, String terminator) { + this(valueType, type, reader, terminator, null); + } + + private Stream( + Class valueType, StreamType type, Reader reader, String terminator, String discriminatorProperty) { + this.valueType = valueType; + this.streamType = type; + this.discriminatorProperty = discriminatorProperty; + if (type == StreamType.JSON) { + this.scanner = new Scanner(reader).useDelimiter(terminator); + this.messageTerminator = terminator; + this.streamTerminator = null; + this.sseReader = null; + } else { + this.scanner = null; + this.messageTerminator = NEWLINE; + this.streamTerminator = terminator; + this.sseReader = reader; + } + } + + public static Stream fromJson(Class valueType, Reader reader, String delimiter) { + return new Stream<>(valueType, reader, delimiter); + } + + public static Stream fromJson(Class valueType, Reader reader) { + return new Stream<>(valueType, reader, NEWLINE); + } + + public static Stream fromSse(Class valueType, Reader sseReader) { + return new Stream<>(valueType, StreamType.SSE, sseReader, null); + } + + public static Stream fromSse(Class valueType, Reader sseReader, String streamTerminator) { + return new Stream<>(valueType, StreamType.SSE, sseReader, streamTerminator); + } + + /** + * Creates a stream from SSE data with event-level discrimination support. + * Use this when the SSE payload is a discriminated union where the discriminator + * is an SSE envelope field (e.g., 'event'). + * + * @param valueType The class of the objects in the stream. + * @param sseReader The reader that provides the SSE data. + * @param discriminatorProperty The property name used for discrimination (e.g., "event"). + * @param The type of objects in the stream. + * @return A new Stream instance configured for SSE with event-level discrimination. + */ + public static Stream fromSseWithEventDiscrimination( + Class valueType, Reader sseReader, String discriminatorProperty) { + return new Stream<>(valueType, StreamType.SSE_EVENT_DISCRIMINATED, sseReader, null, discriminatorProperty); + } + + /** + * Creates a stream from SSE data with event-level discrimination support and a stream terminator. + * + * @param valueType The class of the objects in the stream. + * @param sseReader The reader that provides the SSE data. + * @param discriminatorProperty The property name used for discrimination (e.g., "event"). + * @param streamTerminator The terminator string that signals end of stream (e.g., "[DONE]"). + * @param The type of objects in the stream. + * @return A new Stream instance configured for SSE with event-level discrimination. + */ + public static Stream fromSseWithEventDiscrimination( + Class valueType, Reader sseReader, String discriminatorProperty, String streamTerminator) { + return new Stream<>( + valueType, StreamType.SSE_EVENT_DISCRIMINATED, sseReader, streamTerminator, discriminatorProperty); + } + + @Override + public void close() throws IOException { + if (!isClosed) { + isClosed = true; + if (scanner != null) { + scanner.close(); + } + if (sseReader != null) { + sseReader.close(); + } + } + } + + private boolean isStreamClosed() { + return isClosed; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + switch (streamType) { + case SSE: + return new SSEIterator(); + case SSE_EVENT_DISCRIMINATED: + return new SSEEventDiscriminatedIterator(); + case JSON: + default: + return new JsonIterator(); + } + } + + private final class JsonIterator implements Iterator { + + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + if (isStreamClosed()) { + return false; + } + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (isStreamClosed()) { + throw new NoSuchElementException("Stream is closed"); + } + + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = + ObjectMappers.JSON_MAPPER.readValue(scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private final class SSEIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + + private SSEIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + // Event ID field (ignored) + } else if (line.startsWith("retry:")) { + // Retry field (ignored) + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + } + + /** + * Iterator for SSE streams with event-level discrimination. + * Uses SseEventParser to construct the full SSE envelope for Jackson deserialization. + */ + private final class SSEEventDiscriminatedIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + private String currentEventId = null; + private Long currentRetry = null; + + private SSEEventDiscriminatedIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0 || currentEventType != null) { + try { + // Use SseEventParser for event-level discrimination + nextItem = SseEventParser.parseEventLevelUnion( + currentEventType, + eventDataBuffer.toString(), + currentEventId, + currentRetry, + valueType, + discriminatorProperty); + hasNextItem = true; + resetEventState(); + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + resetEventState(); + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + String idValue = line.length() > 3 ? line.substring(3) : ""; + if (idValue.startsWith(" ")) { + idValue = idValue.substring(1); + } + currentEventId = idValue; + } else if (line.startsWith("retry:")) { + String retryValue = line.length() > 6 ? line.substring(6) : ""; + if (retryValue.startsWith(" ")) { + retryValue = retryValue.substring(1); + } + try { + currentRetry = Long.parseLong(retryValue.trim()); + } catch (NumberFormatException e) { + // Ignore invalid retry values + } + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + // Handle any remaining buffered data at end of stream + if (eventDataBuffer.length() > 0 || currentEventType != null) { + try { + nextItem = SseEventParser.parseEventLevelUnion( + currentEventType, + eventDataBuffer.toString(), + currentEventId, + currentRetry, + valueType, + discriminatorProperty); + hasNextItem = true; + resetEventState(); + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + resetEventState(); + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + + private void resetEventState() { + eventDataBuffer.setLength(0); + currentEventType = null; + currentEventId = null; + currentRetry = null; + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Suppliers.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Suppliers.java new file mode 100644 index 000000000000..edbdf2c793e8 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/AsyncBasicAuthClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/AsyncBasicAuthClient.java new file mode 100644 index 000000000000..b0e6eb6d9d76 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/AsyncBasicAuthClient.java @@ -0,0 +1,54 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.basicauth; + +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.RequestOptions; +import java.util.concurrent.CompletableFuture; + +public class AsyncBasicAuthClient { + protected final ClientOptions clientOptions; + + private final AsyncRawBasicAuthClient rawClient; + + public AsyncBasicAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawBasicAuthClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawBasicAuthClient withRawResponse() { + return this.rawClient; + } + + /** + * GET request with basic auth scheme + */ + public CompletableFuture getWithBasicAuth() { + return this.rawClient.getWithBasicAuth().thenApply(response -> response.body()); + } + + /** + * GET request with basic auth scheme + */ + public CompletableFuture getWithBasicAuth(RequestOptions requestOptions) { + return this.rawClient.getWithBasicAuth(requestOptions).thenApply(response -> response.body()); + } + + /** + * POST request with basic auth scheme + */ + public CompletableFuture postWithBasicAuth(Object request) { + return this.rawClient.postWithBasicAuth(request).thenApply(response -> response.body()); + } + + /** + * POST request with basic auth scheme + */ + public CompletableFuture postWithBasicAuth(Object request, RequestOptions requestOptions) { + return this.rawClient.postWithBasicAuth(request, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/AsyncRawBasicAuthClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/AsyncRawBasicAuthClient.java new file mode 100644 index 000000000000..fae77d3b56ef --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/AsyncRawBasicAuthClient.java @@ -0,0 +1,192 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.basicauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.MediaTypes; +import com.seed.basicAuthPwOmitted.core.ObjectMappers; +import com.seed.basicAuthPwOmitted.core.RequestOptions; +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedApiException; +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedException; +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedHttpResponse; +import com.seed.basicAuthPwOmitted.resources.errors.errors.BadRequest; +import com.seed.basicAuthPwOmitted.resources.errors.errors.UnauthorizedRequest; +import com.seed.basicAuthPwOmitted.resources.errors.types.UnauthorizedRequestErrorBody; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public class AsyncRawBasicAuthClient { + protected final ClientOptions clientOptions; + + public AsyncRawBasicAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * GET request with basic auth scheme + */ + public CompletableFuture> getWithBasicAuth() { + return getWithBasicAuth(null); + } + + /** + * GET request with basic auth scheme + */ + public CompletableFuture> getWithBasicAuth( + RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("basic-auth"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedBasicAuthPwOmittedHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, boolean.class), response)); + return; + } + try { + if (response.code() == 401) { + future.completeExceptionally(new UnauthorizedRequest( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, UnauthorizedRequestErrorBody.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedBasicAuthPwOmittedApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (IOException e) { + future.completeExceptionally( + new SeedBasicAuthPwOmittedException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedBasicAuthPwOmittedException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * POST request with basic auth scheme + */ + public CompletableFuture> postWithBasicAuth(Object request) { + return postWithBasicAuth(request, null); + } + + /** + * POST request with basic auth scheme + */ + public CompletableFuture> postWithBasicAuth( + Object request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("basic-auth"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedBasicAuthPwOmittedException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedBasicAuthPwOmittedHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, boolean.class), response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequest( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedRequest( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, UnauthorizedRequestErrorBody.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedBasicAuthPwOmittedApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (IOException e) { + future.completeExceptionally( + new SeedBasicAuthPwOmittedException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedBasicAuthPwOmittedException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/BasicAuthClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/BasicAuthClient.java new file mode 100644 index 000000000000..832a9d3a36ba --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/BasicAuthClient.java @@ -0,0 +1,53 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.basicauth; + +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.RequestOptions; + +public class BasicAuthClient { + protected final ClientOptions clientOptions; + + private final RawBasicAuthClient rawClient; + + public BasicAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawBasicAuthClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawBasicAuthClient withRawResponse() { + return this.rawClient; + } + + /** + * GET request with basic auth scheme + */ + public boolean getWithBasicAuth() { + return this.rawClient.getWithBasicAuth().body(); + } + + /** + * GET request with basic auth scheme + */ + public boolean getWithBasicAuth(RequestOptions requestOptions) { + return this.rawClient.getWithBasicAuth(requestOptions).body(); + } + + /** + * POST request with basic auth scheme + */ + public boolean postWithBasicAuth(Object request) { + return this.rawClient.postWithBasicAuth(request).body(); + } + + /** + * POST request with basic auth scheme + */ + public boolean postWithBasicAuth(Object request, RequestOptions requestOptions) { + return this.rawClient.postWithBasicAuth(request, requestOptions).body(); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/RawBasicAuthClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/RawBasicAuthClient.java new file mode 100644 index 000000000000..99dd7716445b --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/basicauth/RawBasicAuthClient.java @@ -0,0 +1,151 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.basicauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.basicAuthPwOmitted.core.ClientOptions; +import com.seed.basicAuthPwOmitted.core.MediaTypes; +import com.seed.basicAuthPwOmitted.core.ObjectMappers; +import com.seed.basicAuthPwOmitted.core.RequestOptions; +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedApiException; +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedException; +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedHttpResponse; +import com.seed.basicAuthPwOmitted.resources.errors.errors.BadRequest; +import com.seed.basicAuthPwOmitted.resources.errors.errors.UnauthorizedRequest; +import com.seed.basicAuthPwOmitted.resources.errors.types.UnauthorizedRequestErrorBody; +import java.io.IOException; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RawBasicAuthClient { + protected final ClientOptions clientOptions; + + public RawBasicAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * GET request with basic auth scheme + */ + public SeedBasicAuthPwOmittedHttpResponse getWithBasicAuth() { + return getWithBasicAuth(null); + } + + /** + * GET request with basic auth scheme + */ + public SeedBasicAuthPwOmittedHttpResponse getWithBasicAuth(RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("basic-auth"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedBasicAuthPwOmittedHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, boolean.class), response); + } + try { + if (response.code() == 401) { + throw new UnauthorizedRequest( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UnauthorizedRequestErrorBody.class), + response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedBasicAuthPwOmittedApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (IOException e) { + throw new SeedBasicAuthPwOmittedException("Network error executing HTTP request", e); + } + } + + /** + * POST request with basic auth scheme + */ + public SeedBasicAuthPwOmittedHttpResponse postWithBasicAuth(Object request) { + return postWithBasicAuth(request, null); + } + + /** + * POST request with basic auth scheme + */ + public SeedBasicAuthPwOmittedHttpResponse postWithBasicAuth(Object request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("basic-auth"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedBasicAuthPwOmittedException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedBasicAuthPwOmittedHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, boolean.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequest( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedRequest( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, UnauthorizedRequestErrorBody.class), + response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedBasicAuthPwOmittedApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (IOException e) { + throw new SeedBasicAuthPwOmittedException("Network error executing HTTP request", e); + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/errors/BadRequest.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/errors/BadRequest.java new file mode 100644 index 000000000000..93b7c0181768 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/errors/BadRequest.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.errors.errors; + +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedApiException; +import okhttp3.Response; + +public final class BadRequest extends SeedBasicAuthPwOmittedApiException { + public BadRequest(Object body) { + super("BadRequest", 400, body); + } + + public BadRequest(Object body, Response rawResponse) { + super("BadRequest", 400, body, rawResponse); + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/errors/UnauthorizedRequest.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/errors/UnauthorizedRequest.java new file mode 100644 index 000000000000..e46a7f0f5bc7 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/errors/UnauthorizedRequest.java @@ -0,0 +1,33 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.errors.errors; + +import com.seed.basicAuthPwOmitted.core.SeedBasicAuthPwOmittedApiException; +import com.seed.basicAuthPwOmitted.resources.errors.types.UnauthorizedRequestErrorBody; +import okhttp3.Response; + +public final class UnauthorizedRequest extends SeedBasicAuthPwOmittedApiException { + /** + * The body of the response that triggered the exception. + */ + private final UnauthorizedRequestErrorBody body; + + public UnauthorizedRequest(UnauthorizedRequestErrorBody body) { + super("UnauthorizedRequest", 401, body); + this.body = body; + } + + public UnauthorizedRequest(UnauthorizedRequestErrorBody body, Response rawResponse) { + super("UnauthorizedRequest", 401, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public UnauthorizedRequestErrorBody body() { + return this.body; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/types/UnauthorizedRequestErrorBody.java b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/types/UnauthorizedRequestErrorBody.java new file mode 100644 index 000000000000..4a2ef002632c --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/main/java/com/seed/basicAuthOptional/resources/errors/types/UnauthorizedRequestErrorBody.java @@ -0,0 +1,118 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.resources.errors.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.basicAuthPwOmitted.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = UnauthorizedRequestErrorBody.Builder.class) +public final class UnauthorizedRequestErrorBody { + private final String message; + + private final Map additionalProperties; + + private UnauthorizedRequestErrorBody(String message, Map additionalProperties) { + this.message = message; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("message") + public String getMessage() { + return message; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof UnauthorizedRequestErrorBody && equalTo((UnauthorizedRequestErrorBody) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(UnauthorizedRequestErrorBody other) { + return message.equals(other.message); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.message); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static MessageStage builder() { + return new Builder(); + } + + public interface MessageStage { + _FinalStage message(@NotNull String message); + + Builder from(UnauthorizedRequestErrorBody other); + } + + public interface _FinalStage { + UnauthorizedRequestErrorBody build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements MessageStage, _FinalStage { + private String message; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(UnauthorizedRequestErrorBody other) { + message(other.getMessage()); + return this; + } + + @java.lang.Override + @JsonSetter("message") + public _FinalStage message(@NotNull String message) { + this.message = Objects.requireNonNull(message, "message must not be null"); + return this; + } + + @java.lang.Override + public UnauthorizedRequestErrorBody build() { + return new UnauthorizedRequestErrorBody(message, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/StreamTest.java b/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/StreamTest.java new file mode 100644 index 000000000000..10dfad23934a --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/StreamTest.java @@ -0,0 +1,120 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted; + +import static org.junit.jupiter.api.Assertions.*; + +import com.seed.basicAuthPwOmitted.core.ObjectMappers; +import com.seed.basicAuthPwOmitted.core.Stream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +public final class StreamTest { + @Test + public void testJsonStream() { + List> messages = + Arrays.asList(createMap("message", "hello"), createMap("message", "world")); + List jsonStrings = messages.stream().map(StreamTest::mapToJson).collect(Collectors.toList()); + String input = String.join("\n", jsonStrings); + StringReader jsonInput = new StringReader(input); + Stream jsonStream = Stream.fromJson(Map.class, jsonInput); + int expectedMessages = 2; + int actualMessages = 0; + for (Map jsonObject : jsonStream) { + actualMessages++; + assertTrue(jsonObject.containsKey("message")); + } + assertEquals(expectedMessages, actualMessages); + } + + @Test + public void testSseStream() { + List> events = Arrays.asList(createMap("event", "start"), createMap("event", "end")); + List sseStrings = events.stream().map(StreamTest::mapToSse).collect(Collectors.toList()); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("event")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseStreamWithTerminator() { + List> events = Arrays.asList(createMap("message", "first"), createMap("message", "second")); + List sseStrings = + new ArrayList<>(events.stream().map(StreamTest::mapToSse).collect(Collectors.toList())); + sseStrings.add("data: [DONE]"); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput, "[DONE]"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("message")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseEventDiscriminatedStream() { + List sseStrings = Arrays.asList( + mapToSseWithEvent("start", createMap("status", "pending")), + mapToSseWithEvent("end", createMap("status", "complete"))); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSseWithEventDiscrimination(Map.class, sseInput, "event"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + // Event-level discrimination includes the event field in the parsed result + assertTrue(eventData.containsKey("event")); + assertTrue(eventData.containsKey("data")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testStreamResourceManagement() throws IOException { + StringReader testInput = new StringReader("{\"test\":\"data\"}"); + Stream testStream = Stream.fromJson(Map.class, testInput); + testStream.close(); + assertFalse(testStream.iterator().hasNext()); + } + + private static String mapToJson(Map map) { + try { + return ObjectMappers.JSON_MAPPER.writeValueAsString(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String mapToSse(Map map) { + return "data: " + mapToJson(map); + } + + private static String mapToSseWithEvent(String eventType, Map data) { + return "event: " + eventType + "\n" + "data: " + mapToJson(data); + } + + private static Map createMap(String key, String value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/TestClient.java b/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/TestClient.java new file mode 100644 index 000000000000..935bf1bdd2fa --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/TestClient.java @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted; + +public final class TestClient { + public void test() { + // Add tests here and mark this file in .fernignore + assert true; + } +} diff --git a/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/core/QueryStringMapperTest.java b/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/core/QueryStringMapperTest.java new file mode 100644 index 000000000000..6a59e5418919 --- /dev/null +++ b/seed/java-sdk/basic-auth-pw-omitted/src/test/java/com/seed/basicAuthOptional/core/QueryStringMapperTest.java @@ -0,0 +1,339 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.basicAuthPwOmitted.core; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public final class QueryStringMapperTest { + @Test + public void testObjectWithQuotedString_indexedArrays() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithQuotedString_arraysAsRepeats() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_indexedArrays() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_arraysAsRepeats() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_indexedArrays() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_arraysAsRepeats() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_indexedArrays() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_arraysAsRepeats() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_indexedArrays() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = "objects%5B0%5D%5Bvalue%5D=world&objects%5B0%5D%5Bkey%5D=hello&objects%5B1%5D" + + "%5Bvalue%5D=bar&objects%5B1%5D%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_arraysAsRepeats() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = + "objects%5Bvalue%5D=world&objects%5Bkey%5D=hello&objects%5Bvalue" + "%5D=bar&objects%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_indexedArrays() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = + "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds%5D%5B0%5D=id1&objectwitharray" + + "%5BcontactIds%5D%5B1%5D=id2&objectwitharray%5BcontactIds%5D%5B2%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_arraysAsRepeats() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds" + + "%5D=id1&objectwitharray%5BcontactIds%5D=id2&objectwitharray%5BcontactIds%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + private static String queryString(Map params, boolean arraysAsRepeats) { + HttpUrl.Builder httpUrl = HttpUrl.parse("http://www.fakewebsite.com/").newBuilder(); + params.forEach((paramName, paramValue) -> + QueryStringMapper.addQueryParameter(httpUrl, paramName, paramValue, arraysAsRepeats)); + return httpUrl.build().encodedQuery(); + } +}