-
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 7 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 |
|---|---|---|
|
|
@@ -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()); | ||
|
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 |
||
| } | ||
|
|
@@ -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()); | ||
| } | ||
|
fst-john marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.slack.api.model.annotation; | ||
|
Contributor
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'm thinking we move this to a new package/directory /**
* Provides JSON custom annotation utilities for the classes in this library.
*/
package com.slack.api.util.annotations;
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. Given that these predicates are meant to live on slack model objects I think it makes sense for it to live in the |
||
|
|
||
| public interface FieldPredicate { | ||
| boolean test(Object obj); | ||
|
Contributor
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. Nice job on the interface approach 🚀 but I think the Maybe
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. Yeah that's fair; I went with |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * <p> | ||
| * 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; | ||
| } |
| 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.annotation.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 RequiredAdapterFactory implements TypeAdapterFactory { | ||
|
fst-john marked this conversation as resolved.
Outdated
|
||
| @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); | ||
| } | ||
|
|
||
| private <T> void ensureFieldValidity(T obj, List<RequiredFieldEntry> entries) { | ||
| for (RequiredFieldEntry entry : entries) { | ||
| try { | ||
| Object value = entry.field.get(obj); | ||
| if (!entry.predicate.test(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.