Add "did you mean" to ObjectParser (#50938) (#50985)

Check it out:
```
$ curl -u elastic:password -HContent-Type:application/json -XPOST localhost:9200/test/_update/foo?pretty -d'{
  "dac": {}
}'

{
  "error" : {
    "root_cause" : [
      {
        "type" : "x_content_parse_exception",
        "reason" : "[2:3] [UpdateRequest] unknown field [dac] did you mean [doc]?"
      }
    ],
    "type" : "x_content_parse_exception",
    "reason" : "[2:3] [UpdateRequest] unknown field [dac] did you mean [doc]?"
  },
  "status" : 400
}
```

The tricky thing about implementing this is that x-content doesn't
depend on Lucene. So this works by creating an extension point for the
error message using SPI. Elasticsearch's server module provides the
"spell checking" implementation.
s
This commit is contained in:
Nik Everett 2020-01-14 17:53:41 -05:00 committed by GitHub
parent 4bdab0e985
commit fc5fde7950
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 240 additions and 43 deletions

View File

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.xcontent;
import java.util.ServiceLoader;
/**
* Extension point to customize the error message for unknown fields. We expect
* Elasticsearch to plug a fancy implementation that uses Lucene's spelling
* correction infrastructure to suggest corrections.
*/
public interface ErrorOnUnknown {
/**
* The implementation of this interface that was loaded from SPI.
*/
ErrorOnUnknown IMPLEMENTATION = findImplementation();
/**
* Build the error message to use when {@link ObjectParser} encounters an unknown field.
* @param parserName the name of the thing we're parsing
* @param unknownField the field that we couldn't recognize
* @param candidates the possible fields
*/
String errorMessage(String parserName, String unknownField, Iterable<String> candidates);
/**
* Priority that this error message handler should be used.
*/
int priority();
static ErrorOnUnknown findImplementation() {
ErrorOnUnknown best = new ErrorOnUnknown() {
@Override
public String errorMessage(String parserName, String unknownField, Iterable<String> candidates) {
return "[" + parserName + "] unknown field [" + unknownField + "]";
}
@Override
public int priority() {
return Integer.MIN_VALUE;
}
};
for (ErrorOnUnknown c : ServiceLoader.load(ErrorOnUnknown.class)) {
if (best.priority() < c.priority()) {
best = c;
}
}
return best;
}
}

View File

@ -81,18 +81,17 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
}
private interface UnknownFieldParser<Value, Context> {
void acceptUnknownField(String parserName, String field, XContentLocation location, XContentParser parser,
Value value, Context context) throws IOException;
void acceptUnknownField(ObjectParser<Value, Context> objectParser, String field, XContentLocation location, XContentParser parser,
Value value, Context context) throws IOException;
}
private static <Value, Context> UnknownFieldParser<Value, Context> ignoreUnknown() {
return (n, f, l, p, v, c) -> p.skipChildren();
return (op, f, l, p, v, c) -> p.skipChildren();
}
private static <Value, Context> UnknownFieldParser<Value, Context> errorOnUnknown() {
return (n, f, l, p, v, c) -> {
throw new XContentParseException(l, "[" + n + "] unknown field [" + f + "], parser not found");
return (op, f, l, p, v, c) -> {
throw new XContentParseException(l, ErrorOnUnknown.IMPLEMENTATION.errorMessage(op.name, f, op.fieldParserMap.keySet()));
};
}
@ -104,7 +103,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
}
private static <Value, Context> UnknownFieldParser<Value, Context> consumeUnknownField(UnknownFieldConsumer<Value> consumer) {
return (parserName, field, location, parser, value, context) -> {
return (objectParser, field, location, parser, value, context) -> {
XContentParser.Token t = parser.currentToken();
switch (t) {
case VALUE_STRING:
@ -127,7 +126,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
break;
default:
throw new XContentParseException(parser.getTokenLocation(),
"[" + parserName + "] cannot parse field [" + field + "] with value type [" + t + "]");
"[" + objectParser.name + "] cannot parse field [" + field + "] with value type [" + t + "]");
}
};
}
@ -136,12 +135,13 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
Class<Category> categoryClass,
BiConsumer<Value, ? super Category> consumer
) {
return (parserName, field, location, parser, value, context) -> {
return (objectParser, field, location, parser, value, context) -> {
Category o;
try {
o = parser.namedObject(categoryClass, field, context);
} catch (NamedObjectNotFoundException e) {
throw new XContentParseException(location, "[" + parserName + "] " + e.getBareMessage(), e);
// TODO It'd be lovely if we could the options here but we don't have the right stuff plumbed through. We'll get to it!
throw new XContentParseException(location, "[" + objectParser.name + "] " + e.getBareMessage(), e);
}
consumer.accept(value, o);
};
@ -278,7 +278,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
}
if (fieldParser == null) {
unknownFieldParser.acceptUnknownField(name, currentFieldName, currentPosition, parser, value, context);
unknownFieldParser.acceptUnknownField(this, currentFieldName, currentPosition, parser, value, context);
} else {
fieldParser.assertSupports(name, parser, currentFieldName);
parseSub(parser, fieldParser, currentFieldName, value, context);

View File

@ -207,7 +207,7 @@ public class ObjectParserTests extends ESTestCase {
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
XContentParseException ex = expectThrows(XContentParseException.class, () -> objectParser.parse(parser, s, null));
assertEquals(ex.getMessage(), "[1:2] [the_parser] unknown field [not_supported_field], parser not found");
assertEquals(ex.getMessage(), "[1:2] [the_parser] unknown field [not_supported_field]");
}
}

View File

@ -143,7 +143,6 @@ public class RatedRequestsTests extends ESTestCase {
exception = exception.getCause();
}
assertThat(exception.getMessage(), containsString("unknown field"));
assertThat(exception.getMessage(), containsString("parser not found"));
}
}

View File

@ -0,0 +1,13 @@
---
'Misspelled fields get "did you mean"':
- skip:
version: " - 7.5.99"
reason: Implemented in 7.6
- do:
catch: /\[UpdateRequest\] unknown field \[dac\] did you mean \[doc\]\?/
update:
index: test
id: 1
body:
dac: { foo: baz }
upsert: { foo: bar }

View File

@ -0,0 +1,71 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.xcontent;
import org.apache.lucene.search.spell.LevenshteinDistance;
import org.apache.lucene.util.CollectionUtil;
import org.elasticsearch.common.collect.Tuple;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static java.util.stream.Collectors.toList;
public class SuggestingErrorOnUnknown implements ErrorOnUnknown {
@Override
public String errorMessage(String parserName, String unknownField, Iterable<String> candidates) {
String message = String.format(Locale.ROOT, "[%s] unknown field [%s]", parserName, unknownField);
// TODO it'd be nice to combine this with BaseRestHandler's implementation.
LevenshteinDistance ld = new LevenshteinDistance();
final List<Tuple<Float, String>> scored = new ArrayList<>();
for (String candidate : candidates) {
float distance = ld.getDistance(unknownField, candidate);
if (distance > 0.5f) {
scored.add(new Tuple<>(distance, candidate));
}
}
if (scored.isEmpty()) {
return message;
}
CollectionUtil.timSort(scored, (a, b) -> {
// sort by distance in reverse order, then parameter name for equal distances
int compare = a.v1().compareTo(b.v1());
if (compare != 0) {
return -compare;
}
return a.v2().compareTo(b.v2());
});
List<String> keys = scored.stream().map(Tuple::v2).collect(toList());
StringBuilder builder = new StringBuilder(message).append(" did you mean ");
if (keys.size() == 1) {
builder.append("[").append(keys.get(0)).append("]");
} else {
builder.append("any of ").append(keys.toString());
}
builder.append("?");
return builder.toString();
}
@Override
public int priority() {
return 0;
}
}

View File

@ -0,0 +1 @@
org.elasticsearch.common.xcontent.SuggestingErrorOnUnknown

View File

@ -56,7 +56,7 @@ public class ClusterUpdateSettingsRequestTests extends ESTestCase {
XContentParseException iae = expectThrows(XContentParseException.class,
() -> ClusterUpdateSettingsRequest.fromXContent(createParser(xContentType.xContent(), mutated)));
assertThat(iae.getMessage(),
containsString("[cluster_update_settings_request] unknown field [" + unsupportedField + "], parser not found"));
containsString("[cluster_update_settings_request] unknown field [" + unsupportedField + "]"));
} else {
try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) {
ClusterUpdateSettingsRequest parsedRequest = ClusterUpdateSettingsRequest.fromXContent(parser);

View File

@ -288,7 +288,7 @@ public class UpdateRequestTests extends ESTestCase {
.endObject());
XContentParseException ex = expectThrows(XContentParseException.class, () -> request.fromXContent(contentParser));
assertEquals("[1:2] [UpdateRequest] unknown field [unknown_field], parser not found", ex.getMessage());
assertEquals("[1:2] [UpdateRequest] unknown field [unknown_field]", ex.getMessage());
UpdateRequest request2 = new UpdateRequest("test", "type", "1");
XContentParser unknownObject = createParser(XContentFactory.jsonBuilder()
@ -299,7 +299,7 @@ public class UpdateRequestTests extends ESTestCase {
.endObject()
.endObject());
ex = expectThrows(XContentParseException.class, () -> request2.fromXContent(unknownObject));
assertEquals("[1:76] [UpdateRequest] unknown field [params], parser not found", ex.getMessage());
assertEquals("[1:76] [UpdateRequest] unknown field [params]", ex.getMessage());
}
public void testFetchSourceParsing() throws Exception {

View File

@ -0,0 +1,46 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.xcontent;
import org.elasticsearch.test.ESTestCase;
import java.util.Arrays;
import static org.hamcrest.Matchers.equalTo;
public class SuggestingErrorOnUnknownTests extends ESTestCase {
private String errorMessage(String unknownField, String... candidates) {
return new SuggestingErrorOnUnknown().errorMessage("test", unknownField, Arrays.asList(candidates));
}
public void testNoCandidates() {
assertThat(errorMessage("foo"), equalTo("[test] unknown field [foo]"));
}
public void testBadCandidates() {
assertThat(errorMessage("foo", "bar", "baz"), equalTo("[test] unknown field [foo]"));
}
public void testOneCandidate() {
assertThat(errorMessage("foo", "bar", "fop"), equalTo("[test] unknown field [foo] did you mean [fop]?"));
}
public void testManyCandidate() {
assertThat(errorMessage("foo", "bar", "fop", "fou", "baz"),
equalTo("[test] unknown field [foo] did you mean any of [fop, fou]?"));
}
}

View File

@ -163,7 +163,7 @@ public class HighlightBuilderTests extends ESTestCase {
XContentParseException e = expectParseThrows(XContentParseException.class, "{\n" +
" \"bad_fieldname\" : [ \"field1\" 1 \"field2\" ]\n" +
"}\n");
assertEquals("[2:5] [highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
assertEquals("[2:5] [highlight] unknown field [bad_fieldname]", e.getMessage());
}
{
@ -176,7 +176,7 @@ public class HighlightBuilderTests extends ESTestCase {
"}\n");
assertThat(e.getMessage(), containsString("[highlight] failed to parse field [fields]"));
assertThat(e.getCause().getMessage(), containsString("[fields] failed to parse field [body]"));
assertEquals("[4:9] [highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
assertEquals("[4:9] [highlight_field] unknown field [bad_fieldname]", e.getCause().getCause().getMessage());
}
}
@ -194,7 +194,7 @@ public class HighlightBuilderTests extends ESTestCase {
XContentParseException e = expectParseThrows(XContentParseException.class, "{\n" +
" \"bad_fieldname\" : \"value\"\n" +
"}\n");
assertEquals("[2:5] [highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
assertEquals("[2:5] [highlight] unknown field [bad_fieldname]", e.getMessage());
}
{
@ -207,7 +207,7 @@ public class HighlightBuilderTests extends ESTestCase {
"}\n");
assertThat(e.getMessage(), containsString("[highlight] failed to parse field [fields]"));
assertThat(e.getCause().getMessage(), containsString("[fields] failed to parse field [body]"));
assertEquals("[4:9] [highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
assertEquals("[4:9] [highlight_field] unknown field [bad_fieldname]", e.getCause().getCause().getMessage());
}
}
@ -219,7 +219,7 @@ public class HighlightBuilderTests extends ESTestCase {
XContentParseException e = expectParseThrows(XContentParseException.class, "{\n" +
" \"bad_fieldname\" : { \"field\" : \"value\" }\n \n" +
"}\n");
assertEquals("[2:5] [highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
assertEquals("[2:5] [highlight] unknown field [bad_fieldname]", e.getMessage());
}
{
@ -232,7 +232,7 @@ public class HighlightBuilderTests extends ESTestCase {
"}\n");
assertThat(e.getMessage(), containsString("[highlight] failed to parse field [fields]"));
assertThat(e.getCause().getMessage(), containsString("[fields] failed to parse field [body]"));
assertEquals("[4:9] [highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
assertEquals("[4:9] [highlight_field] unknown field [bad_fieldname]", e.getCause().getCause().getMessage());
}
}

View File

@ -254,7 +254,7 @@ public class QueryRescorerBuilderTests extends ESTestCase {
"}\n";
try (XContentParser parser = createParser(rescoreElement)) {
XContentParseException e = expectThrows(XContentParseException.class, () -> RescorerBuilder.parseFromXContent(parser));
assertEquals("[3:17] [query] unknown field [bad_fieldname], parser not found", e.getMessage());
assertEquals("[3:17] [query] unknown field [bad_fieldname]", e.getMessage());
}
rescoreElement = "{\n" +

View File

@ -339,7 +339,7 @@ public class FieldSortBuilderTests extends AbstractSortTestCase<FieldSortBuilder
parser.nextToken();
XContentParseException e = expectThrows(XContentParseException.class, () -> FieldSortBuilder.fromXContent(parser, ""));
assertEquals("[1:18] [field_sort] unknown field [reverse], parser not found", e.getMessage());
assertEquals("[1:18] [field_sort] unknown field [reverse]", e.getMessage());
}
}

View File

@ -228,7 +228,7 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
parser.nextToken();
XContentParseException e = expectThrows(XContentParseException.class, () -> ScriptSortBuilder.fromXContent(parser, null));
assertEquals("[1:15] [_script] unknown field [bad_field], parser not found", e.getMessage());
assertEquals("[1:15] [_script] unknown field [bad_field]", e.getMessage());
}
}
@ -241,7 +241,7 @@ public class ScriptSortBuilderTests extends AbstractSortTestCase<ScriptSortBuild
parser.nextToken();
XContentParseException e = expectThrows(XContentParseException.class, () -> ScriptSortBuilder.fromXContent(parser, null));
assertEquals("[1:15] [_script] unknown field [bad_field], parser not found", e.getMessage());
assertEquals("[1:15] [_script] unknown field [bad_field]", e.getMessage());
}
}

View File

@ -172,7 +172,7 @@ public class DirectCandidateGeneratorTests extends ESTestCase {
// test unknown field
directGenerator = "{ \"unknown_param\" : \"f1\" }";
assertIllegalXContent(directGenerator, IllegalArgumentException.class,
"[direct_generator] unknown field [unknown_param], parser not found");
"[direct_generator] unknown field [unknown_param]");
// test bad value for field (e.g. size expects an int)
directGenerator = "{ \"size\" : \"xxl\" }";

View File

@ -189,7 +189,7 @@ public abstract class AbstractQueryTestCase<QB extends AbstractQueryBuilder<QB>>
if (expectedException == false) {
throw new AssertionError("unexpected exception when parsing query:\n" + testQuery, e);
}
assertThat(e.getMessage(), containsString("unknown field [newField], parser not found"));
assertThat(e.getMessage(), containsString("unknown field [newField]"));
}
}
}

View File

@ -274,7 +274,7 @@ public class DatafeedConfigTests extends AbstractSerializingTestCase<DatafeedCon
.createParser(xContentRegistry(), DeprecationHandler.THROW_UNSUPPORTED_OPERATION, FUTURE_DATAFEED);
XContentParseException e = expectThrows(XContentParseException.class,
() -> DatafeedConfig.STRICT_PARSER.apply(parser, null).build());
assertEquals("[6:5] [datafeed_config] unknown field [tomorrows_technology_today], parser not found", e.getMessage());
assertEquals("[6:5] [datafeed_config] unknown field [tomorrows_technology_today]", e.getMessage());
}
public void testPastQueryConfigParse() throws IOException {

View File

@ -328,7 +328,7 @@ public class DataFrameAnalyticsConfigTests extends AbstractSerializingTestCase<D
XContentFactory.xContent(XContentType.JSON).createParser(
xContentRegistry(), DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
Exception e = expectThrows(IllegalArgumentException.class, () -> DataFrameAnalyticsConfig.STRICT_PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("unknown field [create_time], parser not found"));
assertThat(e.getMessage(), containsString("unknown field [create_time]"));
}
}
@ -343,7 +343,7 @@ public class DataFrameAnalyticsConfigTests extends AbstractSerializingTestCase<D
XContentFactory.xContent(XContentType.JSON).createParser(
xContentRegistry(), DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
Exception e = expectThrows(IllegalArgumentException.class, () -> DataFrameAnalyticsConfig.STRICT_PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("unknown field [version], parser not found"));
assertThat(e.getMessage(), containsString("unknown field [version]"));
}
}

View File

@ -81,7 +81,7 @@ public class JobTests extends AbstractSerializingTestCase<Job> {
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, FUTURE_JOB);
XContentParseException e = expectThrows(XContentParseException.class,
() -> Job.STRICT_PARSER.apply(parser, null).build());
assertEquals("[4:5] [job_details] unknown field [tomorrows_technology_today], parser not found", e.getMessage());
assertEquals("[4:5] [job_details] unknown field [tomorrows_technology_today]", e.getMessage());
}
public void testFutureMetadataParse() throws IOException {

View File

@ -41,7 +41,7 @@ public class ProcessResultsParserTests extends ESTestCase {
XContentParseException e = expectThrows(XContentParseException.class,
() -> parser.parseResults(inputStream).forEachRemaining(a -> {
}));
assertEquals("[1:3] [test_result] unknown field [unknown], parser not found", e.getMessage());
assertEquals("[1:3] [test_result] unknown field [unknown]", e.getMessage());
}
}

View File

@ -350,7 +350,7 @@ public abstract class RestSqlTestCase extends BaseRestSqlTestCase implements Err
expectBadRequest(() -> {
client().performRequest(request);
return Collections.emptyMap();
}, containsString("unknown field [columnar], parser not found"));
}, containsString("unknown field [columnar]"));
}
public static void expectBadRequest(CheckedSupplier<Map<String, Object>, Exception> code, Matcher<String> errorMessageMatcher) {

View File

@ -87,7 +87,7 @@ setup:
---
"Test put config with security headers in the body":
- do:
catch: /unknown field \[headers\], parser not found/
catch: /unknown field \[headers\]/
ml.put_data_frame_analytics:
id: "data_frame_with_header"
body: >
@ -107,7 +107,7 @@ setup:
"Test put config with create_time in the body":
- do:
catch: /unknown field \[create_time\], parser not found/
catch: /unknown field \[create_time\]/
ml.put_data_frame_analytics:
id: "data_frame_with_create_time"
body: >
@ -126,7 +126,7 @@ setup:
"Test put config with version in the body":
- do:
catch: /unknown field \[version\], parser not found/
catch: /unknown field \[version\]/
ml.put_data_frame_analytics:
id: "data_frame_with_version"
body: >
@ -443,7 +443,7 @@ setup:
"Test put config with unknown top level field":
- do:
catch: /unknown field \[unknown_field\], parser not found/
catch: /unknown field \[unknown_field\]/
ml.put_data_frame_analytics:
id: "unknown_field"
body: >

View File

@ -86,7 +86,7 @@ setup:
---
"Test put datafeed with security headers in the body":
- do:
catch: /unknown field \[headers\], parser not found/
catch: /unknown field \[headers\]/
ml.put_datafeed:
datafeed_id: test-datafeed-1
body: >

View File

@ -171,7 +171,7 @@ setup:
"Try to include headers":
- do:
catch: /unknown field \[headers\], parser not found/
catch: /unknown field \[headers\]/
headers:
Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser
rollup.put_job:

View File

@ -196,7 +196,7 @@ public class TextTemplateTests extends ESTestCase {
TextTemplate.parse(parser);
fail("expected parse exception when encountering an unknown field");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("[script] unknown field [unknown_field], parser not found"));
assertThat(e.getMessage(), containsString("[script] unknown field [unknown_field]"));
}
}
@ -210,7 +210,7 @@ public class TextTemplateTests extends ESTestCase {
XContentParser parser = createParser(JsonXContent.jsonXContent, bytes);
parser.nextToken();
XContentParseException ex = expectThrows(XContentParseException.class, () -> TextTemplate.parse(parser));
assertEquals("[1:2] [script] unknown field [template], parser not found", ex.getMessage());
assertEquals("[1:2] [script] unknown field [template]", ex.getMessage());
}
public void testParserInvalidMissingText() throws Exception {
@ -222,7 +222,7 @@ public class TextTemplateTests extends ESTestCase {
XContentParser parser = createParser(JsonXContent.jsonXContent, bytes);
parser.nextToken();
XContentParseException ex = expectThrows(XContentParseException.class, () -> TextTemplate.parse(parser));
assertEquals("[1:2] [script] unknown field [type], parser not found", ex.getMessage());
assertEquals("[1:2] [script] unknown field [type]", ex.getMessage());
}
public void testNullObject() throws Exception {