diff --git a/slack-api-client/src/main/java/com/slack/api/SlackConfig.java b/slack-api-client/src/main/java/com/slack/api/SlackConfig.java index a3b0707b2..1c72260c7 100644 --- a/slack-api-client/src/main/java/com/slack/api/SlackConfig.java +++ b/slack-api-client/src/main/java/com/slack/api/SlackConfig.java @@ -62,6 +62,9 @@ public void setFailOnUnknownProperties(boolean failOnUnknownProperties) { throwException(); } + @Override + public void setFailOnRequiredProperties(boolean failOnRequiredProperties) { throwException(); } + @Override public void setPrettyResponseLoggingEnabled(boolean prettyResponseLoggingEnabled) { throwException(); @@ -248,6 +251,13 @@ public void setLibraryMaintainerMode(boolean libraryMaintainerMode) { */ private boolean failOnUnknownProperties = false; + /** + * Makes it so that any fields annotated with {@link com.slack.api.model.annotation.Required} that are missing + * or invalid when deserializing responses from the Slack Web API client will throw an exception. + * By default, this is "off", but can be opted into by setting to true. + */ + private boolean failOnRequiredProperties = false; + /** * Slack Web API client verifies the existence of tokens before sending HTTP requests to Slack servers. */ diff --git a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java index 4468f2df4..595b8d83b 100644 --- a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java +++ b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java @@ -32,7 +32,7 @@ private GsonFactory() { public static Gson createSnakeCase() { GsonBuilder gsonBuilder = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); - registerTypeAdapters(gsonBuilder, false); + registerTypeAdapters(gsonBuilder, false, false); return gsonBuilder.create(); } @@ -41,9 +41,10 @@ public static Gson createSnakeCase() { */ public static Gson createSnakeCase(SlackConfig config) { boolean failOnUnknownProps = config.isFailOnUnknownProperties(); + boolean failOnRequiredProperties = config.isFailOnRequiredProperties(); GsonBuilder gsonBuilder = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); - registerTypeAdapters(gsonBuilder, failOnUnknownProps); + registerTypeAdapters(gsonBuilder, failOnUnknownProps, failOnRequiredProperties); if (failOnUnknownProps || config.isLibraryMaintainerMode()) { gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); } @@ -58,8 +59,9 @@ public static Gson createSnakeCase(SlackConfig config) { */ public static Gson createCamelCase(SlackConfig config) { boolean failOnUnknownProps = config.isFailOnUnknownProperties(); + boolean failOnRequiredProperties = config.isFailOnRequiredProperties(); GsonBuilder gsonBuilder = new GsonBuilder(); - registerTypeAdapters(gsonBuilder, failOnUnknownProps); + registerTypeAdapters(gsonBuilder, failOnUnknownProps, failOnRequiredProperties); if (failOnUnknownProps || config.isLibraryMaintainerMode()) { gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); } @@ -69,7 +71,7 @@ public static Gson createCamelCase(SlackConfig config) { return gsonBuilder.create(); } - public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnknownProps) { + public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnknownProps, boolean failOnRequiredProperties) { builder .registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps)) .registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps)) @@ -86,5 +88,9 @@ public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnkno .registerTypeAdapter(AppWorkflow.StepInputValueElementDefault.class, new GsonAppWorkflowStepInputValueDefaultFactory(failOnUnknownProps)) .registerTypeAdapter(LogsResponse.DetailsChangedValue.class, new GsonAuditLogsDetailsChangedValueFactory(failOnUnknownProps)) .registerTypeAdapter(LogsResponse.UserIDs.class, new GsonAuditLogsDetailsUserIDsFactory(failOnUnknownProps)); + + if (failOnRequiredProperties) { + builder.registerTypeAdapterFactory(new RequiredAdapterFactory()); + } } } diff --git a/slack-api-model/src/main/java/com/slack/api/model/annotation/FieldPredicate.java b/slack-api-model/src/main/java/com/slack/api/model/annotation/FieldPredicate.java new file mode 100644 index 000000000..d6d7b29cc --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/annotation/FieldPredicate.java @@ -0,0 +1,5 @@ +package com.slack.api.model.annotation; + +public interface FieldPredicate { + boolean test(Object obj); +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/annotation/IsNotNullFieldPredicate.java b/slack-api-model/src/main/java/com/slack/api/model/annotation/IsNotNullFieldPredicate.java new file mode 100644 index 000000000..90e638701 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/annotation/IsNotNullFieldPredicate.java @@ -0,0 +1,10 @@ +package com.slack.api.model.annotation; + +import java.util.Objects; + +public class IsNotNullFieldPredicate implements FieldPredicate { + @Override + public boolean test(Object obj) { + return !Objects.isNull(obj); + } +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/annotation/Required.java b/slack-api-model/src/main/java/com/slack/api/model/annotation/Required.java new file mode 100644 index 000000000..d741490c9 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/annotation/Required.java @@ -0,0 +1,25 @@ +package com.slack.api.model.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Field-level annotation indicating whether the field is a "required" field or not on the model object. + *
+ * The enforcement of the field's presence in instantiated instances of the model object is accomplished using the + * {@link com.slack.api.util.json.RequiredAdapterFactory} which ensures all fields marked with {@link Required} are + * present during the object deserialization (or serialization) process. Note that the enforcement of this annotation + * is opt-in and defaults to "off". + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Required { + /** + * Optional predicate to evaluate against the field annotated with {@link Required}. By default, all fields + * marked with {@link Required} are checked for null. Primitive field types are initialized by the JVM, and thus + * are never null by default. + */ + Class extends FieldPredicate> validator() default IsNotNullFieldPredicate.class; +} diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/RequiredAdapterFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/RequiredAdapterFactory.java new file mode 100644 index 000000000..c77a258e4 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/util/json/RequiredAdapterFactory.java @@ -0,0 +1,69 @@ +package com.slack.api.util.json; + +import com.google.gson.JsonParseException; +import com.google.gson.Gson; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.slack.api.model.annotation.FieldPredicate; +import com.slack.api.model.annotation.Required; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.io.IOException; + +/** + * Adapter factory for processing objects annotated with {@link Required}. This annotation signals what properties + * of a model object are required, and thus should be expected to be initialized on instantiated instances. For all + * fields on the model objected annotated with {@link Required} applies the {@link FieldPredicate#test(Object)} via the + * specified {@link Required#validator()}. + *
+ * Note that this adapter handles both deserialization (JSON --> POJO) and serialization (POJO --> JSON).
+ */
+public class RequiredAdapterFactory implements TypeAdapterFactory {
+ @Override
+ public