-
Notifications
You must be signed in to change notification settings - Fork 228
Add @Required annotation with optional validator #1558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
b6fc327
61394ab
00d45fa
ead8247
f04be39
8e21407
df4cda2
f40b893
d4ca333
4339fc2
60de10b
e8ca214
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,61 +30,83 @@ private GsonFactory() { | |
| * Most of the Slack APIs' key naming is snake-cased. | ||
| */ | ||
| public static Gson createSnakeCase() { | ||
| GsonBuilder gsonBuilder = new GsonBuilder() | ||
| .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); | ||
| registerTypeAdapters(gsonBuilder, false); | ||
| GsonBuilder gsonBuilder = getSnakeCaseBuilder(); | ||
| configureBuilder(gsonBuilder); | ||
| return gsonBuilder.create(); | ||
| } | ||
|
|
||
| /** | ||
| * Most of the Slack APIs' key naming is snake-cased. | ||
| */ | ||
| public static Gson createSnakeCase(SlackConfig config) { | ||
| boolean failOnUnknownProps = config.isFailOnUnknownProperties(); | ||
| GsonBuilder gsonBuilder = new GsonBuilder() | ||
| .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); | ||
| registerTypeAdapters(gsonBuilder, failOnUnknownProps); | ||
| if (failOnUnknownProps || config.isLibraryMaintainerMode()) { | ||
| gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reassignment is redundant, so removing it |
||
| } | ||
| if (config.isPrettyResponseLoggingEnabled()) { | ||
| gsonBuilder = gsonBuilder.setPrettyPrinting(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing here - there's no need for the re-assignmen |
||
| } | ||
| GsonBuilder gsonBuilder = getSnakeCaseBuilder(); | ||
| configureBuilder(gsonBuilder, config); | ||
| return gsonBuilder.create(); | ||
| } | ||
|
|
||
| /** | ||
| * Mainly used for SCIM APIs. | ||
| */ | ||
| public static Gson createCamelCase(SlackConfig config) { | ||
| boolean failOnUnknownProps = config.isFailOnUnknownProperties(); | ||
| GsonBuilder gsonBuilder = new GsonBuilder(); | ||
| registerTypeAdapters(gsonBuilder, failOnUnknownProps); | ||
| if (failOnUnknownProps || config.isLibraryMaintainerMode()) { | ||
| gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); | ||
| configureBuilder(gsonBuilder, config); | ||
| return gsonBuilder.create(); | ||
| } | ||
|
|
||
| private static GsonBuilder getSnakeCaseBuilder() { | ||
| return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); | ||
| } | ||
|
|
||
| private static void configureBuilder(GsonBuilder builder) { | ||
| configureBuilder(builder, false, false, false, false); | ||
| } | ||
|
|
||
| private static void configureBuilder(GsonBuilder builder, SlackConfig config) { | ||
| configureBuilder( | ||
| builder, | ||
| config.isFailOnUnknownProperties(), | ||
| config.isFailOnRequiredProperties(), | ||
| config.isPrettyResponseLoggingEnabled(), | ||
| config.isLibraryMaintainerMode() | ||
| ); | ||
| } | ||
|
|
||
| private static void configureBuilder( | ||
| GsonBuilder builder, | ||
| boolean failOnUnknownProps, | ||
| boolean failOnRequiredProps, | ||
| boolean isPrettyResponseLoggingEnabled, | ||
| boolean isLibraryMaintainerMode | ||
| ) { | ||
| if (failOnUnknownProps || isLibraryMaintainerMode) { | ||
| builder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); | ||
| } | ||
| if (config.isPrettyResponseLoggingEnabled()) { | ||
| gsonBuilder = gsonBuilder.setPrettyPrinting(); | ||
| if (failOnRequiredProps) { | ||
| builder.registerTypeAdapterFactory(new RequiredPropertyDetectionAdapterFactory()); | ||
| } | ||
| return gsonBuilder.create(); | ||
| if (isPrettyResponseLoggingEnabled) { | ||
| builder.setPrettyPrinting(); | ||
| } | ||
|
|
||
| registerTypeAdapters(builder, failOnUnknownProps); | ||
| } | ||
|
|
||
| public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnknownProps) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know why this is |
||
| builder | ||
| .registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(ContextActionsBlockElement.class, new GsonContextActionsBlockElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory()) | ||
| .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(AppWorkflow.StepInputValue.class, new GsonAppWorkflowStepInputValueFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(AppWorkflow.StepInputValueElementDefault.class, new GsonAppWorkflowStepInputValueDefaultFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(LogsResponse.DetailsChangedValue.class, new GsonAuditLogsDetailsChangedValueFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(LogsResponse.UserIDs.class, new GsonAuditLogsDetailsUserIDsFactory(failOnUnknownProps)); | ||
| .registerTypeAdapter(Instant.class, new JavaTimeInstantFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(File.class, new GsonFileFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(LayoutBlock.class, new GsonLayoutBlockFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(ContextActionsBlockElement.class, new GsonContextActionsBlockElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory()) | ||
| .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(AppWorkflow.StepInputValue.class, new GsonAppWorkflowStepInputValueFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(AppWorkflow.StepInputValueElementDefault.class, new GsonAppWorkflowStepInputValueDefaultFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(LogsResponse.DetailsChangedValue.class, new GsonAuditLogsDetailsChangedValueFactory(failOnUnknownProps)) | ||
| .registerTypeAdapter(LogsResponse.UserIDs.class, new GsonAuditLogsDetailsUserIDsFactory(failOnUnknownProps)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.slack.api.model.annotation; | ||
|
|
||
| import com.slack.api.model.predicate.FieldPredicate; | ||
| import com.slack.api.model.predicate.IsNotNullFieldPredicate; | ||
| import com.slack.api.util.json.RequiredPropertyDetectionAdapterFactory; | ||
|
|
||
| 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. | ||
| * <p> | ||
| * The enforcement of the field's presence in instantiated instances of the model object is accomplished using the | ||
| * {@link RequiredPropertyDetectionAdapterFactory} 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.slack.api.model.predicate; | ||
|
|
||
| /** | ||
| * A functional interface for defining validation predicates against {@link java.lang.reflect.Field}. Used by | ||
| * {@link com.slack.api.model.annotation.Required} during object serialization and deserialization to ensure the | ||
| * field member is "valid" per the defined predicate. | ||
| */ | ||
| @FunctionalInterface | ||
| public interface FieldPredicate { | ||
| boolean validate(Object obj); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.slack.api.model.predicate; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public class IsNotNullFieldPredicate implements FieldPredicate { | ||
| @Override | ||
| public boolean validate(Object obj) { | ||
| return Objects.nonNull(obj); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| 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.predicate.FieldPredicate; | ||
| import com.slack.api.model.annotation.Required; | ||
| import lombok.EqualsAndHashCode; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import java.lang.reflect.Field; | ||
| import java.lang.reflect.InvocationTargetException; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| 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()}. | ||
| * <p> | ||
| * Note that this adapter handles both deserialization (JSON --> POJO) and serialization (POJO --> JSON). | ||
| */ | ||
| public class RequiredPropertyDetectionAdapterFactory implements TypeAdapterFactory { | ||
| @Override | ||
| public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { | ||
| TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type); | ||
| List<RequiredFieldEntry> entries = buildRequiredFieldEntries(type.getRawType()); | ||
|
|
||
| if (entries.isEmpty()) { | ||
| return delegate; | ||
| } | ||
|
|
||
| return new TypeAdapter<T>() { | ||
| @Override | ||
| public void write(JsonWriter out, T value) throws IOException { | ||
| if (value != null) { | ||
| ensureFieldValidity(value, entries); | ||
| } | ||
| delegate.write(out, value); | ||
| } | ||
|
|
||
| @Override | ||
| public T read(JsonReader in) throws IOException { | ||
| T result = delegate.read(in); | ||
| if (result == null) { | ||
| return null; | ||
| } | ||
|
|
||
| ensureFieldValidity(result, entries); | ||
| return result; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Scans the given class for fields annotated with {@link Required}, pre-resolves each field's | ||
| * accessibility and {@link FieldPredicate} instance, and returns an immutable list of entries. | ||
| * This is called once per type during Gson adapter creation. | ||
| */ | ||
| private List<RequiredFieldEntry> buildRequiredFieldEntries(Class<?> clazz) { | ||
| List<RequiredFieldEntry> entries = new ArrayList<>(); | ||
| for (Field field : clazz.getDeclaredFields()) { | ||
| Required annotation = field.getAnnotation(Required.class); | ||
| if (annotation != null) { | ||
| field.setAccessible(true); | ||
| try { | ||
| FieldPredicate predicate = annotation.validator().getDeclaredConstructor().newInstance(); | ||
| entries.add(new RequiredFieldEntry(field, predicate)); | ||
| } catch (NoSuchMethodException | InstantiationException | | ||
| IllegalAccessException | InvocationTargetException e) { | ||
| throw new JsonParseException( | ||
| "Cannot instantiate validator for field: " + field.getName(), e); | ||
| } | ||
| } | ||
| } | ||
| return Collections.unmodifiableList(entries); | ||
|
fst-john marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private <T> void ensureFieldValidity(T obj, List<RequiredFieldEntry> entries) { | ||
| for (RequiredFieldEntry entry : entries) { | ||
| try { | ||
| Object value = entry.field.get(obj); | ||
| if (!entry.predicate.validate(value)) { | ||
| throw new JsonParseException("Required field '" + entry.field.getName() | ||
| + "' failed validation in " + obj.getClass().getSimpleName() | ||
| + " using predicate " + entry.predicate.getClass().getSimpleName()); | ||
| } | ||
| } catch (IllegalAccessException e) { | ||
| throw new JsonParseException( | ||
| "Cannot access field: " + entry.field.getName(), e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Class holding the accessible {@link Field} handle and the cached instance of {@link FieldPredicate}. | ||
| */ | ||
| @RequiredArgsConstructor | ||
| @EqualsAndHashCode | ||
| private static class RequiredFieldEntry { | ||
| final Field field; | ||
| final FieldPredicate predicate; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.