diff --git a/databricks-sdk-java/pom.xml b/databricks-sdk-java/pom.xml index f456d1f10..7ad1e0ab0 100644 --- a/databricks-sdk-java/pom.xml +++ b/databricks-sdk-java/pom.xml @@ -104,6 +104,12 @@ jackson-datatype-jsr310 ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + ${jackson.version} + com.google.auto.value diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksError.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksError.java index 6538b6719..8e072b278 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksError.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksError.java @@ -14,8 +14,6 @@ * unrecoverable way and this exception should be thrown, potentially wrapped in another exception. */ public class DatabricksError extends DatabricksException { - private static final String ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo"; - private final String message; private final Throwable cause; private final String errorCode; private final int statusCode; @@ -51,14 +49,13 @@ private DatabricksError( List details) { super(message, cause); this.errorCode = errorCode; - this.message = message; this.cause = cause; this.statusCode = statusCode; this.details = details == null ? Collections.emptyList() : details; } public List getErrorInfo() { - return this.getDetailsByType(ERROR_INFO_TYPE); + return this.getDetailsByType("type.googleapis.com/google.rpc.ErrorInfo"); } public String getErrorCode() { diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrorBody.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrorBody.java index 4c7e386da..f622430dc 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrorBody.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrorBody.java @@ -1,7 +1,10 @@ package com.databricks.sdk.core.error; +import com.databricks.sdk.core.error.details.ErrorDetails; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -21,6 +24,16 @@ public class ApiErrorBody { public ApiErrorBody() {} + /** + * Constructs an ApiErrorBody from the given parameters. + * + *

The error details are converted to a list of ErrorDetail objects. This only supports the + * ErrorInfo type. + * + * @param errorCode The error code. + * @param message The error message. + * @param scimDetail The SCIM detail. + */ public ApiErrorBody( @JsonProperty("error_code") String errorCode, @JsonProperty("message") String message, @@ -28,14 +41,14 @@ public ApiErrorBody( @JsonProperty("status") String scimStatus, @JsonProperty("scimType") String scimType, @JsonProperty("error") String api12Error, - @JsonProperty("details") List errorDetails) { + @JsonProperty("details") ErrorDetails errorDetails) { this.errorCode = errorCode; this.message = message; this.scimDetail = scimDetail; this.scimStatus = scimStatus; this.scimType = scimType; this.api12Error = api12Error; - this.errorDetails = errorDetails; + this.errorDetails = fromDetails(errorDetails); } public List getErrorDetails() { @@ -93,4 +106,26 @@ public String getApi12Error() { public void setApi12Error(String api12Error) { this.api12Error = api12Error; } + + /** + * Converts the error details to a list of ErrorDetail objects. This only supports the ErrorInfo + * type. + * + * @param details The error details to convert. + * @return A list of ErrorDetail objects. + */ + private static List fromDetails(ErrorDetails details) { + if (details == null) { + return Collections.emptyList(); + } + if (!details.errorInfo().isPresent()) { + return Collections.emptyList(); + } + return Arrays.asList( + new ErrorDetail( + "type.googleapis.com/google.rpc.ErrorInfo", + details.errorInfo().get().reason(), + details.errorInfo().get().domain(), + details.errorInfo().get().metadata())); + } } diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/BadRequest.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/BadRequest.java new file mode 100644 index 000000000..3bae9cd32 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/BadRequest.java @@ -0,0 +1,192 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; + +/** + * BadRequest describes violations in a client request. This error type focuses on the syntactic + * aspects of the request. + * + *

BadRequest errors occur when the request format, structure, or content does not meet the + * service's requirements. This is different from business logic errors or system failures - it + * specifically indicates that the client sent a malformed or invalid request. + * + *

Examples of bad request violations might include: + * + *

    + *
  • Missing required fields + *
  • Invalid field values (wrong type, format, or range) + *
  • Malformed JSON or XML + *
  • Unsupported field combinations + *
  • Invalid enum values + *
  • Field length or size violations + *
+ * + *

This information helps clients: + * + *

    + *
  • Identify what's wrong with their request + *
  • Fix the request format before retrying + *
  • Understand the service's input requirements + *
  • Implement proper input validation + *
+ */ +@AutoValue +@JsonDeserialize(builder = AutoValue_BadRequest.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class BadRequest { + + /** + * Describes all field violations in the request. + * + *

This list contains details about each specific field or aspect of the request that violated + * the service's requirements. Multiple violations can occur if the request has multiple problems. + * + * @return the list of field violations + */ + @JsonProperty("field_violations") + public abstract List fieldViolations(); + + /** + * Creates a new builder for constructing BadRequest instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_BadRequest.Builder(); + } + + /** Builder for constructing BadRequest instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the field violations. + * + * @param fieldViolations the list of field violations + * @return this builder for method chaining + */ + @JsonProperty("field_violations") + public abstract Builder setFieldViolations(List fieldViolations); + + /** + * Builds the BadRequest instance. + * + * @return a new BadRequest instance + */ + public BadRequest build() { + if (fieldViolations() == null) { + setFieldViolations(Collections.emptyList()); + } + return autoBuild(); + } + + abstract List fieldViolations(); + + abstract BadRequest autoBuild(); + } + + /** + * BadRequestFieldViolation describes a specific field violation in a request. + * + *

Each violation provides details about what specific field or aspect of the request was + * invalid and how the client can fix it. + */ + @AutoValue + @JsonDeserialize(builder = AutoValue_BadRequest_BadRequestFieldViolation.Builder.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class BadRequestFieldViolation { + + /** + * A path leading to a field in the request body. + * + *

This field identifies the specific location of the violation within the request structure. + * The path format depends on the request format but typically follows a hierarchical structure. + * + *

Examples of field paths: + * + *

    + *
  • "name" - top-level field + *
  • "user.email" - nested field + *
  • "items[0].id" - array element field + *
  • "metadata.api_key" - deeply nested field + *
  • "settings.notifications.enabled" - multi-level nested field + *
+ * + *

This path helps clients quickly locate and fix the problematic field in their request. + * + * @return the path to the violating field + */ + @JsonProperty("field") + public abstract String field(); + + /** + * A description of why the request element is bad. + * + *

This field provides a human-readable explanation of what's wrong with the field and how to + * fix it. The description should be clear enough for developers to understand and resolve the + * issue. + * + *

Examples of field violation descriptions: + * + *

    + *
  • "Field is required and cannot be empty" + *
  • "Value must be a positive integer" + *
  • "Invalid email format" + *
  • "String length must be between 1 and 100 characters" + *
  • "Unsupported enum value. Must be one of: [A, B, C]" + *
  • "Field cannot contain special characters" + *
  • "Date must be in ISO 8601 format (YYYY-MM-DD)" + *
+ * + * @return description of why the field is invalid + */ + @JsonProperty("description") + public abstract String description(); + + /** + * Creates a new builder for constructing BadRequestFieldViolation instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_BadRequest_BadRequestFieldViolation.Builder(); + } + + /** Builder for constructing BadRequestFieldViolation instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the field path. + * + * @param field the path to the violating field + * @return this builder for method chaining + */ + @JsonProperty("field") + public abstract Builder setField(String field); + + /** + * Sets the violation description. + * + * @param description description of why the field is invalid + * @return this builder for method chaining + */ + @JsonProperty("description") + public abstract Builder setDescription(String description); + + /** + * Builds the BadRequestFieldViolation instance. + * + * @return a new BadRequestFieldViolation instance + */ + public abstract BadRequestFieldViolation build(); + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/DebugInfo.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/DebugInfo.java new file mode 100644 index 000000000..d13c87cc3 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/DebugInfo.java @@ -0,0 +1,122 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; + +/** + * DebugInfo describes additional debugging information. + * + *

This class provides detailed information that can be used by developers and support teams to + * understand what went wrong and where the error occurred. The information is typically more + * technical and detailed than what would be shown to end users. + * + *

DebugInfo is particularly useful for: + * + *

    + *
  • Development and testing environments + *
  • Support ticket investigations + *
  • System debugging and troubleshooting + *
  • Understanding the root cause of errors + *
+ */ +@AutoValue +@JsonDeserialize(builder = AutoValue_DebugInfo.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class DebugInfo { + + /** + * The stack trace entries indicating where the error occurred. + * + *

This list contains the call stack at the time the error occurred, typically starting from + * the most recent call and going backwards through the call chain. Each entry usually represents + * a method call or function invocation. + * + *

Stack trace information is invaluable for: + * + *

    + *
  • Identifying the exact location where an error occurred + *
  • Understanding the execution path that led to the error + *
  • Debugging complex error scenarios + *
  • Providing context to support teams + *
+ * + * @return the stack trace entries + */ + @JsonProperty("stack_entries") + public abstract List stackEntries(); + + /** + * Additional debugging information provided by the server. + * + *

This field can contain any additional context or details that the server deems useful for + * debugging purposes. The exact content depends on the service implementation and the specific + * error that occurred. + * + *

Examples of additional debugging information might include: + * + *

    + *
  • Internal error codes or identifiers + *
  • System state information + *
  • Configuration details + *
  • Performance metrics + *
  • Other diagnostic data + *
+ * + * @return additional debugging information + */ + @JsonProperty("detail") + public abstract String detail(); + + /** + * Creates a new builder for constructing DebugInfo instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_DebugInfo.Builder(); + } + + /** Builder for constructing DebugInfo instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the stack trace entries. + * + * @param stackEntries the stack trace entries + * @return this builder for method chaining + */ + @JsonProperty("stack_entries") + public abstract Builder setStackEntries(List stackEntries); + + /** + * Sets the additional debugging information. + * + * @param detail additional debugging information + * @return this builder for method chaining + */ + @JsonProperty("detail") + public abstract Builder setDetail(String detail); + + /** + * Builds the DebugInfo instance. + * + * @return a new DebugInfo instance + */ + public DebugInfo build() { + if (stackEntries() == null) { + setStackEntries(Collections.emptyList()); + } + return autoBuild(); + } + + abstract List stackEntries(); + + abstract DebugInfo autoBuild(); + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorDetails.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorDetails.java new file mode 100644 index 000000000..cd2714aca --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorDetails.java @@ -0,0 +1,77 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * ErrorDetails contains the error details of an API error. It is the union of known error details + * types and unknown details. + */ +@AutoValue +@JsonDeserialize(using = ErrorDetailsDeserializer.class) +public abstract class ErrorDetails { + + public abstract Optional errorInfo(); + + public abstract Optional requestInfo(); + + public abstract Optional retryInfo(); + + public abstract Optional debugInfo(); + + public abstract Optional quotaFailure(); + + public abstract Optional preconditionFailure(); + + public abstract Optional badRequest(); + + public abstract Optional resourceInfo(); + + public abstract Optional help(); + + public abstract List unknownDetails(); + + public static Builder builder() { + return new AutoValue_ErrorDetails.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setErrorInfo(ErrorInfo errorInfo); + + public abstract Builder setRequestInfo(RequestInfo requestInfo); + + public abstract Builder setRetryInfo(RetryInfo retryInfo); + + public abstract Builder setDebugInfo(DebugInfo debugInfo); + + public abstract Builder setQuotaFailure(QuotaFailure quotaFailure); + + public abstract Builder setPreconditionFailure(PreconditionFailure preconditionFailure); + + public abstract Builder setBadRequest(BadRequest badRequest); + + public abstract Builder setResourceInfo(ResourceInfo resourceInfo); + + public abstract Builder setHelp(Help help); + + public abstract Builder setUnknownDetails(List unknownDetails); + + abstract List unknownDetails(); + + abstract ErrorDetails autoBuild(); + + public ErrorDetails build() { + try { + unknownDetails(); + } catch (IllegalStateException e) { + setUnknownDetails(Collections.emptyList()); + } + return autoBuild(); + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorDetailsDeserializer.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorDetailsDeserializer.java new file mode 100644 index 000000000..867cf9ae3 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorDetailsDeserializer.java @@ -0,0 +1,174 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +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.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ErrorDetailsDeserializer extends JsonDeserializer { + + private static final String ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo"; + private static final String REQUEST_INFO_TYPE = "type.googleapis.com/google.rpc.RequestInfo"; + private static final String RETRY_INFO_TYPE = "type.googleapis.com/google.rpc.RetryInfo"; + private static final String DEBUG_INFO_TYPE = "type.googleapis.com/google.rpc.DebugInfo"; + private static final String QUOTA_FAILURE_TYPE = "type.googleapis.com/google.rpc.QuotaFailure"; + private static final String PRECONDITION_FAILURE_TYPE = + "type.googleapis.com/google.rpc.PreconditionFailure"; + private static final String BAD_REQUEST_TYPE = "type.googleapis.com/google.rpc.BadRequest"; + private static final String RESOURCE_INFO_TYPE = "type.googleapis.com/google.rpc.ResourceInfo"; + private static final String HELP_TYPE = "type.googleapis.com/google.rpc.Help"; + + @Override + public ErrorDetails deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + JsonNode node = mapper.readTree(p); + + if (node.isArray()) { + return deserializeFromArray((ArrayNode) node, mapper); + } else { + // Fallback to single object deserialization + return deserializeFromObject((ObjectNode) node, mapper); + } + } + + private ErrorDetails deserializeFromArray(ArrayNode arrayNode, ObjectMapper mapper) + throws IOException { + ErrorDetails.Builder builder = ErrorDetails.builder(); + List unknownDetails = new ArrayList<>(); + + for (JsonNode detailNode : arrayNode) { + if (detailNode.isObject()) { + ObjectNode objectNode = (ObjectNode) detailNode; + String type = objectNode.path("@type").asText(); + + try { + switch (type) { + case ERROR_INFO_TYPE: + ErrorInfo errorInfo = mapper.treeToValue(objectNode, ErrorInfo.class); + builder.setErrorInfo(errorInfo); + break; + case REQUEST_INFO_TYPE: + RequestInfo requestInfo = mapper.treeToValue(objectNode, RequestInfo.class); + builder.setRequestInfo(requestInfo); + break; + case RETRY_INFO_TYPE: + RetryInfo retryInfo = mapper.treeToValue(objectNode, RetryInfo.class); + builder.setRetryInfo(retryInfo); + break; + case DEBUG_INFO_TYPE: + DebugInfo debugInfo = mapper.treeToValue(objectNode, DebugInfo.class); + builder.setDebugInfo(debugInfo); + break; + case QUOTA_FAILURE_TYPE: + QuotaFailure quotaFailure = mapper.treeToValue(objectNode, QuotaFailure.class); + builder.setQuotaFailure(quotaFailure); + break; + case PRECONDITION_FAILURE_TYPE: + PreconditionFailure preconditionFailure = + mapper.treeToValue(objectNode, PreconditionFailure.class); + builder.setPreconditionFailure(preconditionFailure); + break; + case BAD_REQUEST_TYPE: + BadRequest badRequest = mapper.treeToValue(objectNode, BadRequest.class); + builder.setBadRequest(badRequest); + break; + case RESOURCE_INFO_TYPE: + ResourceInfo resourceInfo = mapper.treeToValue(objectNode, ResourceInfo.class); + builder.setResourceInfo(resourceInfo); + break; + case HELP_TYPE: + Help help = mapper.treeToValue(objectNode, Help.class); + builder.setHelp(help); + break; + default: + // Unknown type, add to unknownDetails + unknownDetails.add(detailNode); + break; + } + } catch (Exception e) { + // If deserialization fails for a known type, treat it as unknown + unknownDetails.add(detailNode); + } + } else { + // Non-object node, add to unknownDetails + unknownDetails.add(detailNode); + } + } + + builder.setUnknownDetails(unknownDetails); + return builder.build(); + } + + private ErrorDetails deserializeFromObject(ObjectNode objectNode, ObjectMapper mapper) + throws IOException { + // Handle single object case - try to determine type and deserialize accordingly + String type = objectNode.path("@type").asText(); + + try { + switch (type) { + case ERROR_INFO_TYPE: + ErrorInfo errorInfo = mapper.treeToValue(objectNode, ErrorInfo.class); + return ErrorDetails.builder() + .setErrorInfo(errorInfo) + .setUnknownDetails(new ArrayList<>()) + .build(); + case REQUEST_INFO_TYPE: + RequestInfo requestInfo = mapper.treeToValue(objectNode, RequestInfo.class); + return ErrorDetails.builder() + .setRequestInfo(requestInfo) + .setUnknownDetails(new ArrayList<>()) + .build(); + case RETRY_INFO_TYPE: + RetryInfo retryInfo = mapper.treeToValue(objectNode, RetryInfo.class); + return ErrorDetails.builder() + .setRetryInfo(retryInfo) + .setUnknownDetails(new ArrayList<>()) + .build(); + case DEBUG_INFO_TYPE: + DebugInfo debugInfo = mapper.treeToValue(objectNode, DebugInfo.class); + return ErrorDetails.builder() + .setDebugInfo(debugInfo) + .setUnknownDetails(new ArrayList<>()) + .build(); + case QUOTA_FAILURE_TYPE: + QuotaFailure quotaFailure = mapper.treeToValue(objectNode, QuotaFailure.class); + return ErrorDetails.builder() + .setQuotaFailure(quotaFailure) + .setUnknownDetails(new ArrayList<>()) + .build(); + case PRECONDITION_FAILURE_TYPE: + PreconditionFailure preconditionFailure = + mapper.treeToValue(objectNode, PreconditionFailure.class); + return ErrorDetails.builder().setPreconditionFailure(preconditionFailure).build(); + case BAD_REQUEST_TYPE: + BadRequest badRequest = mapper.treeToValue(objectNode, BadRequest.class); + return ErrorDetails.builder().setBadRequest(badRequest).build(); + case RESOURCE_INFO_TYPE: + ResourceInfo resourceInfo = mapper.treeToValue(objectNode, ResourceInfo.class); + return ErrorDetails.builder() + .setResourceInfo(resourceInfo) + .setUnknownDetails(new ArrayList<>()) + .build(); + case HELP_TYPE: + Help help = mapper.treeToValue(objectNode, Help.class); + return ErrorDetails.builder().setHelp(help).setUnknownDetails(new ArrayList<>()).build(); + default: + // Unknown type, treat as unknown detail + List unknownDetails = new ArrayList<>(); + unknownDetails.add(objectNode); + return ErrorDetails.builder().setUnknownDetails(unknownDetails).build(); + } + } catch (Exception e) { + // If deserialization fails, treat as unknown detail + List unknownDetails = new ArrayList<>(); + unknownDetails.add(objectNode); + return ErrorDetails.builder().setUnknownDetails(unknownDetails).build(); + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorInfo.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorInfo.java new file mode 100644 index 000000000..53c23d270 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ErrorInfo.java @@ -0,0 +1,125 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.Map; + +/** + * ErrorInfo describes the cause of the error with structured details. + * + *

This class provides structured information about what went wrong, including: + * + *

    + *
  • The reason for the error (a constant value identifying the proximate cause) + *
  • The logical grouping to which the "reason" belongs + *
  • Additional structured details about the error + *
+ * + *

This information can be used by clients to: + * + *

    + *
  • Understand what went wrong + *
  • Provide better error messages to users + *
  • Implement appropriate error handling logic + *
  • Debug issues more effectively + *
+ */ +@AutoValue +@JsonDeserialize(builder = AutoValue_ErrorInfo.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class ErrorInfo { + + /** + * The reason of the error. This is a constant value that identifies the proximate cause of the + * error. + * + *

Examples might include: "INVALID_ARGUMENT", "RESOURCE_NOT_FOUND", "PERMISSION_DENIED", etc. + * + * @return the reason for the error + */ + @JsonProperty("reason") + public abstract String reason(); + + /** + * The logical grouping to which the "reason" belongs. + * + *

This provides context about the domain or category of the error. Examples might include: + * "databricks.api", "databricks.auth", etc. + * + * @return the domain of the error + */ + @JsonProperty("domain") + public abstract String domain(); + + /** + * Additional structured details about this error. + * + *

This map can contain arbitrary key-value pairs that provide additional context about the + * error. The exact contents depend on the specific error type and the service implementation. + * + * @return additional metadata about the error + */ + @JsonProperty("metadata") + public abstract Map metadata(); + + /** + * Creates a new builder for constructing ErrorInfo instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_ErrorInfo.Builder(); + } + + /** Builder for constructing ErrorInfo instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the reason for the error. + * + * @param reason the reason for the error + * @return this builder for method chaining + */ + @JsonProperty("reason") + public abstract Builder setReason(String reason); + + /** + * Sets the domain of the error. + * + * @param domain the domain of the error + * @return this builder for method chaining + */ + @JsonProperty("domain") + public abstract Builder setDomain(String domain); + + /** + * Sets the additional metadata about the error. + * + * @param metadata additional metadata about the error + * @return this builder for method chaining + */ + @JsonProperty("metadata") + public abstract Builder setMetadata(Map metadata); + + /** + * Builds the ErrorInfo instance. + * + * @return a new ErrorInfo instance + */ + public ErrorInfo build() { + if (metadata() == null) { + setMetadata(Collections.emptyMap()); + } + return autoBuild(); + } + + abstract Map metadata(); + + abstract ErrorInfo autoBuild(); + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/Help.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/Help.java new file mode 100644 index 000000000..f631803c2 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/Help.java @@ -0,0 +1,211 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; + +/** + * Help provides links to documentation or for performing an out of band action. + * + *

For example, if a quota check failed with an error indicating the calling project hasn't + * enabled the accessed service, this can contain a URL pointing directly to the right place in the + * developer console to flip the bit. + * + *

Help information is particularly useful for: + * + *

    + *
  • Guiding users to self-service solutions + *
  • Providing context-specific documentation + *
  • Offering actionable steps to resolve issues + *
  • Reducing support ticket volume + *
  • Improving user experience during error scenarios + *
+ * + *

This type of error detail helps transform error responses from simple failure notifications + * into actionable guidance that users can follow to resolve their issues. + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_Help.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Help { + + /** + * URL(s) pointing to additional information on handling the current error. + * + *

This list contains links to resources that can help users understand and resolve the error + * they encountered. Each link should provide specific, actionable guidance related to the current + * error context. + * + *

Examples of helpful links might include: + * + *

    + *
  • Documentation pages explaining the error + *
  • Troubleshooting guides + *
  • Configuration pages in developer consoles + *
  • Support contact forms + *
  • Community forums or knowledge bases + *
  • Video tutorials or walkthroughs + *
  • API reference documentation + *
+ * + * @return the list of helpful links + */ + @JsonProperty("links") + public abstract List links(); + + /** + * Creates a new builder for constructing Help instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_Help.Builder(); + } + + /** Builder for constructing Help instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the helpful links. + * + * @param links the list of helpful links + * @return this builder for method chaining + */ + @JsonProperty("links") + public abstract Builder setLinks(List links); + + /** + * Builds the Help instance. + * + * @return a new Help instance + */ + public Help build() { + if (links() == null) { + setLinks(Collections.emptyList()); + } + return autoBuild(); + } + + abstract List links(); + + abstract Help autoBuild(); + } + + /** + * HelpLink provides a single helpful resource for resolving an error. + * + *

Each link should be specific to the error context and provide actionable guidance that users + * can follow to resolve their issue. + */ + @AutoValue + @JsonDeserialize(builder = AutoValue_Help_HelpLink.Builder.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class HelpLink { + + /** + * Describes what the link offers. + * + *

This field provides a human-readable description of what users can expect to find when + * they follow the link. The description should be clear and specific enough for users to decide + * whether the link is relevant to their situation. + * + *

Examples of link descriptions: + * + *

    + *
  • "Enable the service in the developer console" + *
  • "View API rate limiting documentation" + *
  • "Check your authentication configuration" + *
  • "Learn about required permissions" + *
  • "Troubleshoot common connection issues" + *
  • "Contact support for this error" + *
  • "View example code and usage patterns" + *
  • "Check service status and known issues" + *
+ * + *

A good description helps users understand: + * + *

    + *
  • What the link will help them with + *
  • Whether it's relevant to their specific error + *
  • What they can expect to learn or accomplish + *
+ * + * @return description of what the link offers + */ + @JsonProperty("description") + public abstract String description(); + + /** + * The URL of the link. + * + *

This field contains the actual URL that users can follow to access the helpful resource. + * The URL should be: + * + *

    + *
  • Accessible to the user (not requiring special access) + *
  • Stable and not likely to change frequently + *
  • Secure (HTTPS for web resources) + *
  • Relevant to the current error context + *
+ * + *

Examples of helpful URLs: + * + *

    + *
  • Documentation pages: "https://docs.example.com/errors/rate-limit" + *
  • Developer console: "https://console.example.com/settings/api" + *
  • Support forms: "https://support.example.com/contact" + *
  • Knowledge base: "https://help.example.com/troubleshooting" + *
  • Community forums: "https://community.example.com/errors" + *
+ * + * @return the URL of the helpful resource + */ + @JsonProperty("url") + public abstract String url(); + + /** + * Creates a new builder for constructing HelpLink instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_Help_HelpLink.Builder(); + } + + /** Builder for constructing HelpLink instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the link description. + * + * @param description description of what the link offers + * @return this builder for method chaining + */ + @JsonProperty("description") + public abstract Builder setDescription(String description); + + /** + * Sets the link URL. + * + * @param url the URL of the helpful resource + * @return this builder for method chaining + */ + @JsonProperty("url") + public abstract Builder setUrl(String url); + + /** + * Builds the HelpLink instance. + * + * @return a new HelpLink instance + */ + public abstract HelpLink build(); + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/PreconditionFailure.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/PreconditionFailure.java new file mode 100644 index 000000000..ca5a16692 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/PreconditionFailure.java @@ -0,0 +1,210 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; + +/** + * PreconditionFailure describes what preconditions have failed. + * + *

This error type indicates that one or more conditions that must be met before a request can be + * processed have not been satisfied. Preconditions are typically business logic rules or system + * state requirements that must be true for the operation to succeed. + * + *

Examples of precondition failures might include: + * + *

    + *
  • Terms of service not accepted + *
  • Required configuration not set + *
  • System not in the correct state + *
  • Required permissions not granted + *
  • Dependencies not satisfied + *
+ * + *

This information helps clients understand: + * + *

    + *
  • What conditions need to be met + *
  • How to resolve the precondition issues + *
  • What actions they need to take + *
  • Whether the issue is resolvable by the client + *
+ */ +@AutoValue +@JsonDeserialize(builder = AutoValue_PreconditionFailure.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class PreconditionFailure { + + /** + * Describes all precondition violations. + * + *

This list contains details about each specific precondition that failed. Multiple violations + * can occur if multiple preconditions are not met simultaneously. + * + * @return the list of precondition violations + */ + @JsonProperty("violations") + public abstract List violations(); + + /** + * Creates a new builder for constructing PreconditionFailure instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_PreconditionFailure.Builder(); + } + + /** Builder for constructing PreconditionFailure instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the precondition violations. + * + * @param violations the list of precondition violations + * @return this builder for method chaining + */ + @JsonProperty("violations") + public abstract Builder setViolations(List violations); + + /** + * Builds the PreconditionFailure instance. + * + * @return a new PreconditionFailure instance + */ + public PreconditionFailure build() { + if (violations() == null) { + setViolations(Collections.emptyList()); + } + return autoBuild(); + } + + abstract List violations(); + + abstract PreconditionFailure autoBuild(); + } + + /** + * PreconditionFailureViolation describes a specific precondition violation. + * + *

Each violation provides details about what specific precondition failed and how the client + * can resolve the issue. + */ + @AutoValue + @JsonDeserialize( + builder = AutoValue_PreconditionFailure_PreconditionFailureViolation.Builder.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class PreconditionFailureViolation { + + /** + * The type of PreconditionFailure. + * + *

This field categorizes the type of precondition that failed. Examples might include: + * + *

    + *
  • "TERMS_OF_SERVICE" + *
  • "CONFIGURATION" + *
  • "SYSTEM_STATE" + *
  • "PERMISSIONS" + *
  • "DEPENDENCIES" + *
+ * + * @return the type of precondition failure + */ + @JsonProperty("type") + public abstract String type(); + + /** + * The subject, relative to the type, that failed. + * + *

This field identifies the specific entity or resource that failed the precondition check. + * The interpretation depends on the type of precondition failure. + * + *

Examples: + * + *

    + *
  • For "TERMS_OF_SERVICE" type: user ID or account identifier + *
  • For "CONFIGURATION" type: configuration key or setting name + *
  • For "PERMISSIONS" type: resource name or permission scope + *
  • For "DEPENDENCIES" type: service name or component identifier + *
+ * + * @return the subject of the precondition failure + */ + @JsonProperty("subject") + public abstract String subject(); + + /** + * A description of how the precondition failed. Developers can use this description to + * understand how to fix the failure. + * + *

Examples of precondition failure descriptions: + * + *

    + *
  • "Terms of service not accepted" + *
  • "Required configuration 'api_key' not set" + *
  • "System is currently in maintenance mode" + *
  • "Insufficient permissions to access resource" + *
  • "Required service 'authentication' is not available" + *
+ * + * @return description of the precondition failure + */ + @JsonProperty("description") + public abstract String description(); + + /** + * Creates a new builder for constructing PreconditionFailureViolation instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_PreconditionFailure_PreconditionFailureViolation.Builder(); + } + + /** Builder for constructing PreconditionFailureViolation instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the type of precondition failure. + * + * @param type the type of precondition failure + * @return this builder for method chaining + */ + @JsonProperty("type") + public abstract Builder setType(String type); + + /** + * Sets the subject of the precondition failure. + * + * @param subject the subject of the precondition failure + * @return this builder for method chaining + */ + @JsonProperty("subject") + public abstract Builder setSubject(String subject); + + /** + * Sets the description of the precondition failure. + * + * @param description description of the precondition failure + * @return this builder for method chaining + */ + @JsonProperty("description") + public abstract Builder setDescription(String description); + + /** + * Builds the PreconditionFailureViolation instance. + * + * @return a new PreconditionFailureViolation instance + */ + public abstract PreconditionFailureViolation build(); + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/QuotaFailure.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/QuotaFailure.java new file mode 100644 index 000000000..1e5c2fa22 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/QuotaFailure.java @@ -0,0 +1,174 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; + +/** + * QuotaFailure describes how a quota check failed. + * + *

For example, if a daily limit was exceeded for the calling project, a service could respond + * with a QuotaFailure detail containing the project id and the description of the quota limit that + * was exceeded. If the calling project hasn't enabled the service in the developer console, then a + * service could respond with the project id and set {@code service_disabled} to true. + * + *

Also see {@link RetryInfo} and {@link Help} types for other details about handling a quota + * failure. + * + *

This information helps clients understand: + * + *

    + *
  • What quota limits were exceeded + *
  • How to resolve quota-related issues + *
  • What actions they can take to continue + *
  • Whether they need to contact support + *
+ */ +@AutoValue +@JsonDeserialize(builder = AutoValue_QuotaFailure.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class QuotaFailure { + + /** + * Describes all quota violations. + * + *

This list contains details about each specific quota violation that occurred. Multiple + * violations can happen in a single request if multiple quota limits are exceeded simultaneously. + * + * @return the list of quota violations + */ + @JsonProperty("violations") + public abstract List violations(); + + /** + * Creates a new builder for constructing QuotaFailure instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_QuotaFailure.Builder(); + } + + /** Builder for constructing QuotaFailure instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the quota violations. + * + * @param violations the list of quota violations + * @return this builder for method chaining + */ + @JsonProperty("violations") + public abstract Builder setViolations(List violations); + + /** + * Builds the QuotaFailure instance. + * + * @return a new QuotaFailure instance + */ + public QuotaFailure build() { + if (violations() == null) { + setViolations(Collections.emptyList()); + } + return autoBuild(); + } + + abstract List violations(); + + abstract QuotaFailure autoBuild(); + } + + /** + * QuotaFailureViolation describes a specific quota violation. + * + *

Each violation provides details about what specific quota limit was exceeded and how the + * client can resolve the issue. + */ + @AutoValue + @JsonDeserialize(builder = AutoValue_QuotaFailure_QuotaFailureViolation.Builder.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class QuotaFailureViolation { + + /** + * The subject on which the quota check failed. + * + *

This typically identifies the specific resource, project, user, or other entity that + * exceeded the quota limit. Examples might include: + * + *

    + *
  • Project ID + *
  • User ID + *
  • Resource name + *
  • Service identifier + *
+ * + * @return the subject of the quota violation + */ + @JsonProperty("subject") + public abstract String subject(); + + /** + * A description of how the quota check failed. Clients can use this description to find more + * about the quota configuration in the service's public documentation, or find the relevant + * quota limit to adjust through developer console. + * + *

Examples of quota violation descriptions: + * + *

    + *
  • "Service disabled" + *
  • "Daily Limit for read operations exceeded" + *
  • "Rate limit exceeded: 100 requests per minute" + *
  • "Storage quota exceeded: 1GB limit" + *
+ * + * @return description of the quota violation + */ + @JsonProperty("description") + public abstract String description(); + + /** + * Creates a new builder for constructing QuotaFailureViolation instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_QuotaFailure_QuotaFailureViolation.Builder(); + } + + /** Builder for constructing QuotaFailureViolation instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the subject of the quota violation. + * + * @param subject the subject of the quota violation + * @return this builder for method chaining + */ + @JsonProperty("subject") + public abstract Builder setSubject(String subject); + + /** + * Sets the description of the quota violation. + * + * @param description description of the quota violation + * @return this builder for method chaining + */ + @JsonProperty("description") + public abstract Builder setDescription(String description); + + /** + * Builds the QuotaFailureViolation instance. + * + * @return a new QuotaFailureViolation instance + */ + public abstract QuotaFailureViolation build(); + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/RequestInfo.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/RequestInfo.java new file mode 100644 index 000000000..d691d2195 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/RequestInfo.java @@ -0,0 +1,99 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; + +/** + * RequestInfo contains metadata about the request that clients can attach when filing a bug or + * providing other forms of feedback. + * + *

This class provides information that can be used to: + * + *

    + *
  • Identify specific requests in service logs for debugging + *
  • Provide context when reporting issues to support teams + *
  • Correlate client-side errors with server-side logs + *
  • Enable more effective troubleshooting of issues + *
+ * + *

The information contained here is typically opaque to clients and should only be interpreted + * by the service that generated it. + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_RequestInfo.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class RequestInfo { + + /** + * An opaque string that should only be interpreted by the service that generated it. For example, + * it can be used to identify requests in the service's logs. + * + *

This identifier is typically used by service operators and support teams to trace the + * execution of specific requests through the system. + * + * @return the request identifier + */ + @JsonProperty("request_id") + public abstract String requestId(); + + /** + * Any data that was used to serve this request. For example, an encrypted stack trace that can be + * sent back to the service provider for debugging. + * + *

This field may contain sensitive information that is useful for debugging but should be + * handled carefully. It might include: + * + *

    + *
  • Stack traces or error logs + *
  • Request context or parameters + *
  • System state information + *
  • Other diagnostic data + *
+ * + * @return data that was used to serve the request + */ + @JsonProperty("serving_data") + public abstract String servingData(); + + /** + * Creates a new builder for constructing RequestInfo instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_RequestInfo.Builder(); + } + + /** Builder for constructing RequestInfo instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the request identifier. + * + * @param requestId the request identifier + * @return this builder for method chaining + */ + @JsonProperty("request_id") + public abstract Builder setRequestId(String requestId); + + /** + * Sets the serving data. + * + * @param servingData data that was used to serve the request + * @return this builder for method chaining + */ + @JsonProperty("serving_data") + public abstract Builder setServingData(String servingData); + + /** + * Builds the RequestInfo instance. + * + * @return a new RequestInfo instance + */ + public abstract RequestInfo build(); + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ResourceInfo.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ResourceInfo.java new file mode 100644 index 000000000..f47375b7a --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/ResourceInfo.java @@ -0,0 +1,189 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; + +/** + * ResourceInfo describes the resource that is being accessed. + * + *

This error detail type provides information about the specific resource that was involved in + * the error. This is particularly useful when the error is related to resource access, permissions, + * or resource-specific operations. + * + *

ResourceInfo helps clients understand: + * + *

    + *
  • What resource was involved in the error + *
  • What type of resource it was + *
  • Who owns the resource (if applicable) + *
  • What specific error occurred when accessing the resource + *
+ * + *

Examples of when ResourceInfo might be provided: + * + *

    + *
  • Permission denied errors + *
  • Resource not found errors + *
  • Resource conflict errors + *
  • Resource quota exceeded errors + *
  • Resource state validation errors + *
+ */ +@AutoValue +@JsonDeserialize(builder = AutoValue_ResourceInfo.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class ResourceInfo { + + /** + * A name for the type of resource being accessed. + * + *

This field categorizes the resource by its type or class. Examples might include: + * + *

    + *
  • "workspace" + *
  • "cluster" + *
  • "job" + *
  • "table" + *
  • "user" + *
  • "group" + *
  • "secret" + *
  • "notebook" + *
+ * + *

This information helps clients understand what category of resource was involved and + * potentially implement resource-type-specific error handling. + * + * @return the type of resource + */ + @JsonProperty("resource_type") + public abstract String resourceType(); + + /** + * The name of the resource being accessed. + * + *

This field provides the specific identifier or name of the resource that was involved in the + * error. The format depends on the resource type but typically includes: + * + *

    + *
  • Resource IDs (e.g., "12345") + *
  • Resource names (e.g., "my-cluster") + *
  • Resource paths (e.g., "/path/to/resource") + *
  • Resource URIs (e.g., "databricks://workspace/123") + *
+ * + *

This identifier helps clients locate the specific resource and potentially retry the + * operation or report the issue more precisely. + * + * @return the name or identifier of the resource + */ + @JsonProperty("resource_name") + public abstract String resourceName(); + + /** + * The owner of the resource (optional). + * + *

This field identifies who owns or controls the resource. This information is particularly + * useful for permission-related errors or when the client needs to contact the resource owner. + * + *

Examples of owner values: + * + *

    + *
  • User email addresses + *
  • User IDs + *
  • Group names + *
  • Service account identifiers + *
  • Organization names + *
+ * + *

Note: This field may be null or empty if the owner information is not available or not + * applicable. + * + * @return the owner of the resource, or null if not available + */ + @JsonProperty("owner") + public abstract String owner(); + + /** + * Describes what error is encountered when accessing this resource. + * + *

This field provides a human-readable description of the specific error that occurred when + * trying to access or operate on the resource. The description should be clear enough for + * developers to understand what went wrong. + * + *

Examples of resource error descriptions: + * + *

    + *
  • "Resource not found" + *
  • "Access denied: insufficient permissions" + *
  • "Resource is currently locked by another operation" + *
  • "Resource quota exceeded" + *
  • "Resource is in an invalid state for this operation" + *
  • "Resource name contains invalid characters" + *
  • "Resource already exists with this name" + *
+ * + * @return description of the resource access error + */ + @JsonProperty("description") + public abstract String description(); + + /** + * Creates a new builder for constructing ResourceInfo instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_ResourceInfo.Builder(); + } + + /** Builder for constructing ResourceInfo instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the resource type. + * + * @param resourceType the type of resource + * @return this builder for method chaining + */ + @JsonProperty("resource_type") + public abstract Builder setResourceType(String resourceType); + + /** + * Sets the resource name. + * + * @param resourceName the name or identifier of the resource + * @return this builder for method chaining + */ + @JsonProperty("resource_name") + public abstract Builder setResourceName(String resourceName); + + /** + * Sets the resource owner. + * + * @param owner the owner of the resource + * @return this builder for method chaining + */ + @JsonProperty("owner") + public abstract Builder setOwner(String owner); + + /** + * Sets the error description. + * + * @param description description of the resource access error + * @return this builder for method chaining + */ + @JsonProperty("description") + public abstract Builder setDescription(String description); + + /** + * Builds the ResourceInfo instance. + * + * @return a new ResourceInfo instance + */ + public abstract ResourceInfo build(); + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/RetryInfo.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/RetryInfo.java new file mode 100644 index 000000000..a44d9a141 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/details/RetryInfo.java @@ -0,0 +1,145 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.io.IOException; +import java.time.Duration; + +/** + * RetryInfo describes when clients can retry a failed request. + * + *

Clients could ignore the recommendation here or retry when this information is missing from + * error responses. However, it's always recommended that clients should use exponential backoff + * when retrying. + * + *

Clients should wait until the {@code retryDelay} amount of time has passed since receiving the + * error response before retrying. If retrying requests also fail, clients should use an exponential + * backoff scheme to gradually increase the delay between retries based on {@code retryDelay}, until + * either a maximum number of retries have been reached or a maximum retry delay cap has been + * reached. + * + *

This information helps clients implement intelligent retry strategies that respect the + * service's recommendations while avoiding overwhelming the service with rapid retry attempts. + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_RetryInfo.Builder.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class RetryInfo { + + /** + * Clients should wait at least this long between retrying the same request. + * + *

Note: This is serialized as a string in the format "3.000000001s" where the string ends in + * the suffix "s" (indicating seconds) and is preceded by a decimal number of seconds. + * + *

Examples of valid formats: + * + *

    + *
  • "30s" - 30 seconds + *
  • "1.5s" - 1.5 seconds + *
  • "0.001s" - 1 millisecond + *
  • "3.000000001s" - 3 seconds and 1 nanosecond + *
+ * + * @return the recommended delay before retrying + */ + @JsonProperty("retry_delay") + @JsonDeserialize(using = RetryInfo.DurationDeserializer.class) + public abstract Duration retryDelay(); + + /** + * Creates a new builder for constructing RetryInfo instances. + * + * @return a new builder instance + */ + public static Builder builder() { + return new AutoValue_RetryInfo.Builder(); + } + + /** Builder for constructing RetryInfo instances. */ + @AutoValue.Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public abstract static class Builder { + + /** + * Sets the retry delay. + * + * @param retryDelay the recommended delay before retrying + * @return this builder for method chaining + */ + @JsonProperty("retry_delay") + @JsonDeserialize(using = RetryInfo.DurationDeserializer.class) + public abstract Builder setRetryDelay(Duration retryDelay); + + /** + * Builds the RetryInfo instance. + * + * @return a new RetryInfo instance + */ + public abstract RetryInfo build(); + } + + /** + * Custom deserializer for Duration field to handle "30s" format. + * + *

This deserializer is strict and will reject invalid duration formats by throwing an + * IOException. Valid formats must: + * + *

    + *
  • Be a string value + *
  • End with the 's' suffix + *
  • Contain a valid decimal number before the suffix + *
+ * + *

Examples of valid formats: "30s", "1.5s", "0.001s", "3.000000001s" Examples of invalid + * formats: "30", "30ms", "abc", 42 + */ + static class DurationDeserializer extends JsonDeserializer { + @Override + public Duration deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + if (!node.isTextual()) { + throw new IOException("Duration must be a string, got: " + node.getNodeType()); + } + + String delayStr = node.asText(); + if (!delayStr.endsWith("s")) { + throw new IOException("Duration must end with 's' suffix: " + delayStr); + } + + String numeric = delayStr.substring(0, delayStr.length() - 1).trim(); + if (numeric.isEmpty()) { + throw new IOException("Invalid duration format: " + delayStr); + } + if (numeric.startsWith("-")) { + throw new IOException("Duration must be non-negative: " + delayStr); + } + + int dotIdx = numeric.indexOf('.'); + String secondsPart = dotIdx >= 0 ? numeric.substring(0, dotIdx) : numeric; + String fractionPart = dotIdx >= 0 ? numeric.substring(dotIdx + 1) : ""; + + if (fractionPart.length() > 9) { + throw new IOException("Fractional seconds precision exceeds nanoseconds: " + delayStr); + } + + String fractionPadded = + fractionPart.isEmpty() ? "" : (fractionPart + "000000000").substring(0, 9); + + try { + long seconds = secondsPart.isEmpty() ? 0L : Long.parseLong(secondsPart); + long nanos = fractionPadded.isEmpty() ? 0L : Long.parseLong(fractionPadded); + return Duration.ofSeconds(seconds, nanos); + } catch (NumberFormatException e) { + throw new IOException("Invalid duration format: " + delayStr, e); + } + } + } +} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java index 9730b135e..8fd484996 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.guava.GuavaModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; /** Utilities for serialization and deserialization in the Databricks Java SDK. */ @@ -12,6 +13,7 @@ public static ObjectMapper createMapper() { ObjectMapper mapper = new ObjectMapper(); mapper .registerModule(new JavaTimeModule()) + .registerModule(new GuavaModule()) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/ApiClientTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/ApiClientTest.java index bad3dfadc..ef51f8363 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/ApiClientTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/ApiClientTest.java @@ -5,6 +5,8 @@ import com.databricks.sdk.core.error.ApiErrorBody; import com.databricks.sdk.core.error.ErrorDetail; import com.databricks.sdk.core.error.PrivateLinkValidationError; +import com.databricks.sdk.core.error.details.ErrorDetails; +import com.databricks.sdk.core.error.details.ErrorInfo; import com.databricks.sdk.core.http.Request; import com.databricks.sdk.core.http.Response; import com.databricks.sdk.core.utils.FakeTimer; @@ -317,10 +319,24 @@ void errorDetails() throws JsonProcessingException { Map metadata = new HashMap<>(); metadata.put("etag", "value"); - ErrorDetail errorDetails = - new ErrorDetail("type.googleapis.com/google.rpc.ErrorInfo", "reason", "domain", metadata); - ErrorDetail unrelatedDetails = - new ErrorDetail("unrelated", "wrong", "wrongDomain", new HashMap<>()); + + // Create ErrorDetails object instead of List + ErrorDetails errorDetails = + ErrorDetails.builder() + .setErrorInfo( + ErrorInfo.builder() + .setReason("reason") + .setDomain("domain") + .setMetadata(metadata) + .build()) + .setUnknownDetails( + Arrays.asList( + mapper + .createObjectNode() + .put("@type", "unrelated") + .put("reason", "wrong") + .put("domain", "wrongDomain"))) + .build(); DatabricksError error = runFailingApiClientTest( @@ -329,24 +345,17 @@ void errorDetails() throws JsonProcessingException { getTransientError( req, 401, - new ApiErrorBody( - "ERROR", - null, - null, - null, - null, - null, - Arrays.asList(errorDetails, unrelatedDetails))), + new ApiErrorBody("ERROR", null, null, null, null, null, errorDetails)), getSuccessResponse(req)), MyEndpointResponse.class, DatabricksError.class); List responseErrors = error.getErrorInfo(); assertEquals(responseErrors.size(), 1); ErrorDetail responseError = responseErrors.get(0); - assertEquals(errorDetails.getType(), responseError.getType()); - assertEquals(errorDetails.getReason(), responseError.getReason()); - assertEquals(errorDetails.getMetadata(), responseError.getMetadata()); - assertEquals(errorDetails.getDomain(), responseError.getDomain()); + assertEquals("type.googleapis.com/google.rpc.ErrorInfo", responseError.getType()); + assertEquals("reason", responseError.getReason()); + assertEquals(metadata, responseError.getMetadata()); + assertEquals("domain", responseError.getDomain()); } @Test diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/details/ErrorDetailsTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/details/ErrorDetailsTest.java new file mode 100644 index 000000000..4a877a651 --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/details/ErrorDetailsTest.java @@ -0,0 +1,430 @@ +package com.databricks.sdk.core.error.details; + +import static org.junit.jupiter.api.Assertions.*; + +import com.databricks.sdk.core.utils.SerDeUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import java.time.Duration; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +public class ErrorDetailsTest { + + private final ObjectMapper mapper = SerDeUtils.createMapper(); + + @Test + public void testDeserializeFromArray() throws Exception { + String json = + "[" + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\"," + + " \"reason\": \"PERMISSION_DENIED\"," + + " \"domain\": \"databricks.com\"," + + " \"metadata\": {" + + " \"service\": \"workspace\"" + + " }" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.RequestInfo\"," + + " \"request_id\": \"req-123\"," + + " \"serving_data\": \"stack-trace-data\"" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.RetryInfo\"," + + " \"retry_delay\": \"30s\"" + + " }," + + " {" + + " \"@type\": \"unknown.type\"," + + " \"custom_field\": \"custom_value\"" + + " }" + + "]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + JsonNode unknownDetail = + mapper.readTree("{\"@type\": \"unknown.type\", \"custom_field\": \"custom_value\"}"); + + ErrorDetails expected = + ErrorDetails.builder() + .setErrorInfo( + ErrorInfo.builder() + .setReason("PERMISSION_DENIED") + .setDomain("databricks.com") + .setMetadata( + ImmutableMap.builder().put("service", "workspace").build()) + .build()) + .setRequestInfo( + RequestInfo.builder() + .setRequestId("req-123") + .setServingData("stack-trace-data") + .build()) + .setRetryInfo(RetryInfo.builder().setRetryDelay(Duration.ofSeconds(30)).build()) + .setUnknownDetails(Arrays.asList(unknownDetail)) + .build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeFromSingleObject() throws Exception { + String json = + "{" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\"," + + " \"reason\": \"RESOURCE_EXHAUSTED\"," + + " \"domain\": \"databricks.com\"," + + " \"metadata\": {" + + " \"quota_limit\": \"1000\"" + + " }" + + "}"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + ErrorDetails expected = + ErrorDetails.builder() + .setErrorInfo( + ErrorInfo.builder() + .setReason("RESOURCE_EXHAUSTED") + .setDomain("databricks.com") + .setMetadata( + ImmutableMap.builder().put("quota_limit", "1000").build()) + .build()) + .build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeFromArrayWithMixedTypes() throws Exception { + String json = + "[" + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.QuotaFailure\"," + + " \"violations\": [" + + " {" + + " \"subject\": \"project:123\"," + + " \"description\": \"Daily limit exceeded\"" + + " }" + + " ]" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.Help\"," + + " \"links\": [" + + " {" + + " \"description\": \"Quota documentation\"," + + " \"url\": \"https://docs.databricks.com/quota\"" + + " }" + + " ]" + + " }," + + " {" + + " \"unrecognized_field\": \"value\"" + + " }" + + "]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + JsonNode unknownDetail = mapper.readTree("{\"unrecognized_field\": \"value\"}"); + + ErrorDetails expected = + ErrorDetails.builder() + .setQuotaFailure( + QuotaFailure.builder() + .setViolations( + Arrays.asList( + QuotaFailure.QuotaFailureViolation.builder() + .setSubject("project:123") + .setDescription("Daily limit exceeded") + .build())) + .build()) + .setHelp( + Help.builder() + .setLinks( + Arrays.asList( + Help.HelpLink.builder() + .setDescription("Quota documentation") + .setUrl("https://docs.databricks.com/quota") + .build())) + .build()) + .setUnknownDetails(Arrays.asList(unknownDetail)) + .build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeEmptyArray() throws Exception { + String json = "[]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + ErrorDetails expected = ErrorDetails.builder().build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeAllErrorDetailTypes() throws Exception { + String json = + "[" + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\"," + + " \"reason\": \"reason\"," + + " \"domain\": \"domain\"," + + " \"metadata\": {\"k1\": \"v1\", \"k2\": \"v2\"}" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.RequestInfo\"," + + " \"request_id\": \"req42\"," + + " \"serving_data\": \"data\"" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.RetryInfo\"," + + " \"retry_delay\": \"1.000000001s\"" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.DebugInfo\"," + + " \"stack_entries\": [\"entry1\", \"entry2\"]," + + " \"detail\": \"detail\"" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.QuotaFailure\"," + + " \"violations\": [{\"subject\": \"subject\", \"description\": \"description\"}]" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.PreconditionFailure\"," + + " \"violations\": [{\"type\": \"type\", \"subject\": \"subject\", \"description\": \"description\"}]" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.BadRequest\"," + + " \"field_violations\": [{\"field\": \"field\", \"description\": \"description\"}]" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ResourceInfo\"," + + " \"resource_type\": \"resource_type\"," + + " \"resource_name\": \"resource_name\"," + + " \"owner\": \"owner\"," + + " \"description\": \"description\"" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.Help\"," + + " \"links\": [{\"description\": \"description\", \"url\": \"url\"}]" + + " }" + + "]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + ErrorDetails expected = + ErrorDetails.builder() + .setErrorInfo( + ErrorInfo.builder() + .setReason("reason") + .setDomain("domain") + .setMetadata( + ImmutableMap.builder() + .put("k1", "v1") + .put("k2", "v2") + .build()) + .build()) + .setRequestInfo( + RequestInfo.builder().setRequestId("req42").setServingData("data").build()) + .setRetryInfo(RetryInfo.builder().setRetryDelay(Duration.ofSeconds(1, 1)).build()) + .setDebugInfo( + DebugInfo.builder() + .setStackEntries(Arrays.asList("entry1", "entry2")) + .setDetail("detail") + .build()) + .setQuotaFailure( + QuotaFailure.builder() + .setViolations( + Arrays.asList( + QuotaFailure.QuotaFailureViolation.builder() + .setSubject("subject") + .setDescription("description") + .build())) + .build()) + .setPreconditionFailure( + PreconditionFailure.builder() + .setViolations( + Arrays.asList( + PreconditionFailure.PreconditionFailureViolation.builder() + .setType("type") + .setSubject("subject") + .setDescription("description") + .build())) + .build()) + .setBadRequest( + BadRequest.builder() + .setFieldViolations( + Arrays.asList( + BadRequest.BadRequestFieldViolation.builder() + .setField("field") + .setDescription("description") + .build())) + .build()) + .setResourceInfo( + ResourceInfo.builder() + .setResourceType("resource_type") + .setResourceName("resource_name") + .setOwner("owner") + .setDescription("description") + .build()) + .setHelp( + Help.builder() + .setLinks( + Arrays.asList( + Help.HelpLink.builder() + .setDescription("description") + .setUrl("url") + .build())) + .build()) + .build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeUnknownErrorDetailType() throws Exception { + String json = + "[" + " {" + " \"@type\": \"foo\"," + " \"reason\": \"reason\"" + " }" + "]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + JsonNode unknownDetail = mapper.readTree("{\"@type\": \"foo\", \"reason\": \"reason\"}"); + + ErrorDetails expected = + ErrorDetails.builder().setUnknownDetails(Arrays.asList(unknownDetail)).build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeInvalidErrorDetails() throws Exception { + String json = + "[" + + " 42," + + " \"foobar\"," + + " {\"foo\": \"bar\"}," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\"," + + " \"reason\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.RequestInfo\"," + + " \"request_id\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.RetryInfo\"," + + " \"retry_delay\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.DebugInfo\"," + + " \"stack_entries\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.QuotaFailure\"," + + " \"violations\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.PreconditionFailure\"," + + " \"violations\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.BadRequest\"," + + " \"field_violations\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ResourceInfo\"," + + " \"resource_type\": 0" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.Help\"," + + " \"links\": 0" + + " }" + + "]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + JsonNode firstUnknown = mapper.readTree("42"); + JsonNode secondUnknown = mapper.readTree("\"foobar\""); + JsonNode thirdUnknown = mapper.readTree("{\"foo\": \"bar\"}"); + JsonNode errorInfoUnknown = + mapper.readTree("{\"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\", \"reason\": 0}"); + JsonNode requestInfoUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.RequestInfo\", \"request_id\": 0}"); + JsonNode retryInfoUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.RetryInfo\", \"retry_delay\": 0}"); + JsonNode debugInfoUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.DebugInfo\", \"stack_entries\": 0}"); + JsonNode quotaFailureUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.QuotaFailure\", \"violations\": 0}"); + JsonNode preconditionFailureUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.PreconditionFailure\", \"violations\": 0}"); + JsonNode badRequestUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.BadRequest\", \"field_violations\": 0}"); + JsonNode resourceInfoUnknown = + mapper.readTree( + "{\"@type\": \"type.googleapis.com/google.rpc.ResourceInfo\", \"resource_type\": 0}"); + JsonNode helpUnknown = + mapper.readTree("{\"@type\": \"type.googleapis.com/google.rpc.Help\", \"links\": 0}"); + + ErrorDetails expected = + ErrorDetails.builder() + .setUnknownDetails( + Arrays.asList( + firstUnknown, + secondUnknown, + thirdUnknown, + errorInfoUnknown, + requestInfoUnknown, + retryInfoUnknown, + debugInfoUnknown, + quotaFailureUnknown, + preconditionFailureUnknown, + badRequestUnknown, + resourceInfoUnknown, + helpUnknown)) + .build(); + + assertEquals(expected, errorDetails); + } + + @Test + public void testDeserializeLastErrorDetailWins() throws Exception { + String json = + "[" + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\"," + + " \"reason\": \"first\"," + + " \"domain\": \"test\"," + + " \"metadata\": {}" + + " }," + + " {" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\"," + + " \"reason\": \"second\"," + + " \"domain\": \"test\"," + + " \"metadata\": {}" + + " }" + + "]"; + + ErrorDetails errorDetails = mapper.readValue(json, ErrorDetails.class); + + ErrorDetails expected = + ErrorDetails.builder() + .setErrorInfo( + ErrorInfo.builder() + .setReason("second") + .setDomain("test") + .setMetadata(ImmutableMap.of()) + .build()) + .build(); + + assertEquals(expected, errorDetails); + } +} diff --git a/src/main/java/com/databricks/sdk/core/error/details/ErrorDetails.java b/src/main/java/com/databricks/sdk/core/error/details/ErrorDetails.java new file mode 100644 index 000000000..46e32a1ad --- /dev/null +++ b/src/main/java/com/databricks/sdk/core/error/details/ErrorDetails.java @@ -0,0 +1,75 @@ +package com.databricks.sdk.core.error.details; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * ErrorDetails contains the error details of an API error. It is the union of known error details + * types and unknown details. + */ +@AutoValue +@JsonDeserialize(using = ErrorDetailsDeserializer.class) +public abstract class ErrorDetails { + + public abstract Optional errorInfo(); + + public abstract Optional requestInfo(); + + public abstract Optional retryInfo(); + + public abstract Optional debugInfo(); + + public abstract Optional quotaFailure(); + + public abstract Optional preconditionFailure(); + + public abstract Optional badRequest(); + + public abstract Optional resourceInfo(); + + public abstract Optional help(); + + public abstract List unknownDetails(); + + public static Builder builder() { + return new AutoValue_ErrorDetails.Builder().setUnknownDetails(Collections.emptyList()); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setErrorInfo(ErrorInfo errorInfo); + + public abstract Builder setRequestInfo(RequestInfo requestInfo); + + public abstract Builder setRetryInfo(RetryInfo retryInfo); + + public abstract Builder setDebugInfo(DebugInfo debugInfo); + + public abstract Builder setQuotaFailure(QuotaFailure quotaFailure); + + public abstract Builder setPreconditionFailure(PreconditionFailure preconditionFailure); + + public abstract Builder setBadRequest(BadRequest badRequest); + + public abstract Builder setResourceInfo(ResourceInfo resourceInfo); + + public abstract Builder setHelp(Help help); + + public abstract Builder setUnknownDetails(List unknownDetails); + + abstract ErrorDetails autoBuild(); + + public ErrorDetails build() { + if (unknownDetails() == null) { + setUnknownDetails(Collections.emptyList()); + } + return autoBuild(); + } + + abstract List unknownDetails(); + } +}