-
Notifications
You must be signed in to change notification settings - Fork 211
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
Improve validation error messages for unknown property and json mapping exceptions #5044
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package org.opensearch.dataprepper.plugin; | ||
|
||
import com.fasterxml.jackson.databind.JsonMappingException; | ||
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; | ||
import org.apache.commons.text.similarity.LevenshteinDistance; | ||
import org.opensearch.dataprepper.model.configuration.PluginSetting; | ||
import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException; | ||
|
||
import javax.inject.Inject; | ||
import javax.inject.Named; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.stream.Collectors; | ||
|
||
@Named | ||
public class PluginConfigurationErrorHandler { | ||
|
||
static final String UNRECOGNIZED_PROPERTY_EXCEPTION_FORMAT = "Parameter \"%s\" for plugin \"%s\" does not exist. Available options include %s."; | ||
static final String JSON_MAPPING_EXCEPTION_FORMAT = "Parameter \"%s\" for plugin \"%s\" is invalid: %s"; | ||
static final String GENERIC_PLUGIN_EXCEPTION_FORMAT = "Plugin \"%s\" is invalid: %s"; | ||
|
||
static final Integer MIN_DISTANCE_TO_RECOMMEND_PROPERTY = 3; | ||
|
||
private final LevenshteinDistance levenshteinDistance; | ||
|
||
@Inject | ||
public PluginConfigurationErrorHandler(final LevenshteinDistance levenshteinDistance) { | ||
this.levenshteinDistance = levenshteinDistance; | ||
} | ||
|
||
public RuntimeException handleException(final PluginSetting pluginSetting, final Exception e) { | ||
if (e.getCause() instanceof UnrecognizedPropertyException) { | ||
return handleUnrecognizedPropertyException((UnrecognizedPropertyException) e.getCause(), pluginSetting); | ||
} else if (e.getCause() instanceof JsonMappingException) { | ||
return handleJsonMappingException((JsonMappingException) e.getCause(), pluginSetting); | ||
} | ||
|
||
return new InvalidPluginConfigurationException( | ||
String.format(GENERIC_PLUGIN_EXCEPTION_FORMAT, pluginSetting.getName(), e.getMessage())); | ||
} | ||
|
||
private RuntimeException handleJsonMappingException(final JsonMappingException e, final PluginSetting pluginSetting) { | ||
final String parameterPath = getParameterPath(e.getPath()); | ||
|
||
final String errorMessage = String.format(JSON_MAPPING_EXCEPTION_FORMAT, | ||
parameterPath, pluginSetting.getName(), e.getOriginalMessage()); | ||
|
||
return new InvalidPluginConfigurationException(errorMessage); | ||
} | ||
|
||
private RuntimeException handleUnrecognizedPropertyException(final UnrecognizedPropertyException e, final PluginSetting pluginSetting) { | ||
String errorMessage = String.format(UNRECOGNIZED_PROPERTY_EXCEPTION_FORMAT, | ||
getParameterPath(e.getPath()), | ||
pluginSetting.getName(), | ||
e.getKnownPropertyIds()); | ||
|
||
final Optional<String> closestRecommendation = getClosestField(e); | ||
|
||
if (closestRecommendation.isPresent()) { | ||
errorMessage += " Did you mean \"" + closestRecommendation.get() + "\"?"; | ||
} | ||
|
||
return new InvalidPluginConfigurationException(errorMessage); | ||
} | ||
|
||
private Optional<String> getClosestField(final UnrecognizedPropertyException e) { | ||
String closestMatch = null; | ||
int smallestDistance = Integer.MAX_VALUE; | ||
|
||
for (final String field : e.getKnownPropertyIds().stream().map(Object::toString).collect(Collectors.toList())) { | ||
int distance = levenshteinDistance.apply(e.getPropertyName(), field); | ||
|
||
if (distance < smallestDistance) { | ||
smallestDistance = distance; | ||
closestMatch = field; | ||
} | ||
} | ||
|
||
if (smallestDistance <= MIN_DISTANCE_TO_RECOMMEND_PROPERTY) { | ||
return Optional.ofNullable(closestMatch); | ||
} | ||
|
||
return Optional.empty(); | ||
} | ||
|
||
private String getParameterPath(final List<JsonMappingException.Reference> path) { | ||
return path.stream() | ||
.map(JsonMappingException.Reference::getFieldName) | ||
.collect(Collectors.joining(".")); | ||
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. Maybe we should join with 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 agree with consistency, but maybe the consistent thing to do would be to use 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 ok with either as long as it is consistent. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this should come first.
Perhaps:
When not present, it could be:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually feel like it is easier to see the recommendation when it's at the end with just a glance. If the entire message is read in order then the recommendation coming first is good, but it does seem easier to see when it's at the end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entry-pipeline:processor:parse_json: caused by: Parameter "destintio" for plugin "parse_json" does not exist. Available options include [tags_on_failure, handle_failed_events, parse_when, source, pointer, destination, delete_source, overwrite_if_destination_exists]. Did you mean "destination"?
vs.
entry-pipeline:processor:parse_json: caused by: Parameter "destintio" for plugin "parse_json" does not exist. Did you mean "destination"? Other available options include [tags_on_failure, handle_failed_events, parse_when, source, pointer, destination, delete_source, overwrite_if_destination_exists].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, that makes sense.
We probably need to make better use of newlines. Then it would be much cleaner.