-
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
Open
fst-john
wants to merge
11
commits into
slackapi:main
Choose a base branch
from
fst-john:fstj_add_required_annotation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+349
−7
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
b6fc327
impl
fst-john 61394ab
update javadocs
fst-john 00d45fa
updates
fst-john ead8247
update javadocs
fst-john f04be39
small update
fst-john 8e21407
add a couple more test cases
fst-john df4cda2
cache FieldPredicate instances upfront
fst-john f40b893
address comments
fst-john d4ca333
oops
fst-john 4339fc2
addressing more comments
fst-john 60de10b
update
fst-john File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,12 +44,17 @@ public static Gson createSnakeCase(SlackConfig config) { | |
| GsonBuilder gsonBuilder = new GsonBuilder() | ||
| .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); | ||
| registerTypeAdapters(gsonBuilder, failOnUnknownProps); | ||
|
|
||
| if (failOnUnknownProps || config.isLibraryMaintainerMode()) { | ||
| gsonBuilder = gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); | ||
|
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 |
||
| gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); | ||
| } | ||
| if (config.isFailOnRequiredProperties()) { | ||
| gsonBuilder.registerTypeAdapterFactory(new RequiredPropertyDetectionAdapterFactory()); | ||
| } | ||
| if (config.isPrettyResponseLoggingEnabled()) { | ||
| gsonBuilder = gsonBuilder.setPrettyPrinting(); | ||
|
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.setPrettyPrinting(); | ||
| } | ||
|
|
||
| return gsonBuilder.create(); | ||
| } | ||
|
|
||
|
|
@@ -60,12 +65,17 @@ 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()); | ||
| gsonBuilder.registerTypeAdapterFactory(new UnknownPropertyDetectionAdapterFactory()); | ||
| } | ||
| if (config.isFailOnRequiredProperties()) { | ||
| gsonBuilder.registerTypeAdapterFactory(new RequiredPropertyDetectionAdapterFactory()); | ||
| } | ||
| if (config.isPrettyResponseLoggingEnabled()) { | ||
| gsonBuilder = gsonBuilder.setPrettyPrinting(); | ||
| gsonBuilder.setPrettyPrinting(); | ||
| } | ||
|
|
||
| return gsonBuilder.create(); | ||
| } | ||
|
|
||
|
|
||
29 changes: 29 additions & 0 deletions
29
slack-api-model/src/main/java/com/slack/api/model/annotation/Required.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
11 changes: 11 additions & 0 deletions
11
slack-api-model/src/main/java/com/slack/api/model/predicate/FieldPredicate.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
10 changes: 10 additions & 0 deletions
10
slack-api-model/src/main/java/com/slack/api/model/predicate/IsNotNullFieldPredicate.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
111 changes: 111 additions & 0 deletions
111
...-model/src/main/java/com/slack/api/util/json/RequiredPropertyDetectionAdapterFactory.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
Show resolved
Hide 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.