Docs: Support triple quotes (#28915)

Adds support for triple quoted strings to the documentation test
generator. Kibana's CONSOLE tool has supported them for a year but we
were unable to use them in Elasticsearch's docs because the process that
converts example snippets into tests couldn't handle this. This change
adds code to convert them into standard JSON so we can pass them to
Elasticsearch.
This commit is contained in:
Nik Everett 2018-03-16 12:46:39 -04:00 committed by GitHub
parent 708c06896b
commit 762226bee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 9 deletions

View File

@ -19,6 +19,7 @@
package org.elasticsearch.gradle.doc
import groovy.transform.PackageScope
import org.elasticsearch.gradle.doc.SnippetsTask.Snippet
import org.gradle.api.InvalidUserDataException
import org.gradle.api.tasks.Input
@ -99,6 +100,43 @@ public class RestTestsFromSnippetsTask extends SnippetsTask {
return snippet.language == 'js' || snippet.curl
}
/**
* Converts Kibana's block quoted strings into standard JSON. These
* {@code """} delimited strings can be embedded in CONSOLE and can
* contain newlines and {@code "} without the normal JSON escaping.
* This has to add it.
*/
@PackageScope
static String replaceBlockQuote(String body) {
int start = body.indexOf('"""');
if (start < 0) {
return body
}
/*
* 1.3 is a fairly wild guess of the extra space needed to hold
* the escaped string.
*/
StringBuilder result = new StringBuilder((int) (body.length() * 1.3));
int startOfNormal = 0;
while (start >= 0) {
int end = body.indexOf('"""', start + 3);
if (end < 0) {
throw new InvalidUserDataException(
"Invalid block quote starting at $start in:\n$body")
}
result.append(body.substring(startOfNormal, start));
result.append('"');
result.append(body.substring(start + 3, end)
.replace('"', '\\"')
.replace("\n", "\\n"));
result.append('"');
startOfNormal = end + 3;
start = body.indexOf('"""', startOfNormal);
}
result.append(body.substring(startOfNormal));
return result.toString();
}
private class TestBuilder {
private static final String SYNTAX = {
String method = /(?<method>GET|PUT|POST|HEAD|OPTIONS|DELETE)/
@ -259,6 +297,8 @@ public class RestTestsFromSnippetsTask extends SnippetsTask {
if (body != null) {
// Throw out the leading newline we get from parsing the body
body = body.substring(1)
// Replace """ quoted strings with valid json ones
body = replaceBlockQuote(body)
current.println(" body: |")
body.eachLine { current.println(" $it") }
}

View File

@ -0,0 +1,50 @@
/*
* 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.gradle.doc
import org.elasticsearch.gradle.doc.SnippetsTask.Snippet
import org.gradle.api.InvalidUserDataException
import static org.elasticsearch.gradle.doc.RestTestsFromSnippetsTask.replaceBlockQuote
class RestTestFromSnippetsTaskTest extends GroovyTestCase {
void testInvalidBlockQuote() {
String input = "\"foo\": \"\"\"bar\"";
String message = shouldFail({ replaceBlockQuote(input) });
assertEquals("Invalid block quote starting at 7 in:\n$input", message);
}
void testSimpleBlockQuote() {
assertEquals("\"foo\": \"bort baz\"",
replaceBlockQuote("\"foo\": \"\"\"bort baz\"\"\""));
}
void testMultipleBlockQuotes() {
assertEquals("\"foo\": \"bort baz\", \"bar\": \"other\"",
replaceBlockQuote("\"foo\": \"\"\"bort baz\"\"\", \"bar\": \"\"\"other\"\"\""));
}
void testEscapingInBlockQuote() {
assertEquals("\"foo\": \"bort\\\" baz\"",
replaceBlockQuote("\"foo\": \"\"\"bort\" baz\"\"\""));
assertEquals("\"foo\": \"bort\\n baz\"",
replaceBlockQuote("\"foo\": \"\"\"bort\n baz\"\"\""));
}
}

View File

@ -53,7 +53,13 @@ GET hockey/_search
"script_score": {
"script": {
"lang": "painless",
"source": "int total = 0; for (int i = 0; i < doc['goals'].length; ++i) { total += doc['goals'][i]; } return total;"
"source": """
int total = 0;
for (int i = 0; i < doc['goals'].length; ++i) {
total += doc['goals'][i];
}
return total;
"""
}
}
}
@ -75,7 +81,13 @@ GET hockey/_search
"total_goals": {
"script": {
"lang": "painless",
"source": "int total = 0; for (int i = 0; i < doc['goals'].length; ++i) { total += doc['goals'][i]; } return total;"
"source": """
int total = 0;
for (int i = 0; i < doc['goals'].length; ++i) {
total += doc['goals'][i];
}
return total;
"""
}
}
}
@ -157,7 +169,10 @@ POST hockey/player/1/_update
{
"script": {
"lang": "painless",
"source": "ctx._source.last = params.last; ctx._source.nick = params.nick",
"source": """
ctx._source.last = params.last;
ctx._source.nick = params.nick
""",
"params": {
"last": "gaudreau",
"nick": "hockey"
@ -228,7 +243,13 @@ POST hockey/player/_update_by_query
{
"script": {
"lang": "painless",
"source": "if (ctx._source.last =~ /b/) {ctx._source.last += \"matched\"} else {ctx.op = 'noop'}"
"source": """
if (ctx._source.last =~ /b/) {
ctx._source.last += "matched";
} else {
ctx.op = "noop";
}
"""
}
}
----------------------------------------------------------------
@ -243,7 +264,13 @@ POST hockey/player/_update_by_query
{
"script": {
"lang": "painless",
"source": "if (ctx._source.last ==~ /[^aeiou].*[aeiou]/) {ctx._source.last += \"matched\"} else {ctx.op = 'noop'}"
"source": """
if (ctx._source.last ==~ /[^aeiou].*[aeiou]/) {
ctx._source.last += "matched";
} else {
ctx.op = "noop";
}
"""
}
}
----------------------------------------------------------------
@ -296,7 +323,10 @@ POST hockey/player/_update_by_query
{
"script": {
"lang": "painless",
"source": "ctx._source.last = ctx._source.last.replaceAll(/[aeiou]/, m -> m.group().toUpperCase(Locale.ROOT))"
"source": """
ctx._source.last = ctx._source.last.replaceAll(/[aeiou]/, m ->
m.group().toUpperCase(Locale.ROOT))
"""
}
}
----------------------------------------------------------------
@ -311,7 +341,10 @@ POST hockey/player/_update_by_query
{
"script": {
"lang": "painless",
"source": "ctx._source.last = ctx._source.last.replaceFirst(/[aeiou]/, m -> m.group().toUpperCase(Locale.ROOT))"
"source": """
ctx._source.last = ctx._source.last.replaceFirst(/[aeiou]/, m ->
m.group().toUpperCase(Locale.ROOT))
"""
}
}
----------------------------------------------------------------

View File

@ -563,7 +563,7 @@ to set the index that the document will be indexed into:
--------------------------------------------------
// NOTCONSOLE
Dynamic field names are also supported. This example sets the field named after the
Dynamic field names are also supported. This example sets the field named after the
value of `service` to the value of the field `code`:
[source,js]
@ -1829,7 +1829,10 @@ PUT _ingest/pipeline/my_index
"processors": [
{
"script": {
"source": " ctx._index = 'my_index'; ctx._type = '_doc' "
"source": """
ctx._index = 'my_index';
ctx._type = '_doc';
"""
}
}
]