All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xbib.gradle.task.elasticsearch.doc.SnippetsTask.groovy Maven / Gradle / Ivy

Go to download

Gradle plugins for the developer kit for building and testing Elasticsearch and Elasticsearch plugins

The newest version!
package org.xbib.gradle.task.elasticsearch.doc

import groovy.json.JsonException
import groovy.json.JsonParserType
import groovy.json.JsonSlurper
import org.gradle.api.DefaultTask
import org.gradle.api.InvalidUserDataException
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction

import java.nio.file.Path
import java.util.regex.Matcher

/**
 * A task which will run a closure on each snippet in the documentation.
 */
class SnippetsTask extends DefaultTask {
    private static final String SCHAR = /(?:\\\/|[^\/])/
    private static final String SUBSTITUTION = /s\/($SCHAR+)\/($SCHAR*)\//
    private static final String CATCH = /catch:\s*((?:\/[^\/]+\/)|[^ \]]+)/
    private static final String SKIP = /skip:([^\]]+)/
    private static final String SETUP = /setup:([^ \]]+)/
    private static final String WARNING = /warning:(.+)/
    private static final String CAT = /(_cat)/
    private static final String TEST_SYNTAX =
        /(?:$CATCH|$SUBSTITUTION|$SKIP|(continued)|$SETUP|$WARNING) ?/

    /**
     * Action to take on each snippet. Called with a single parameter, an
     * instance of Snippet.
     */
    Closure perSnippet

    /**
     * The docs to scan. Defaults to every file in the directory exception the
     * build.gradle file because that is appropriate for Elasticsearch's docs
     * directory.
     */
    @InputFiles
    ConfigurableFileTree docs = project.fileTree(project.projectDir) {
        // No snippets in the build file
        exclude 'build.gradle'
        // That is where the snippets go, not where they come from!
        exclude 'build'
    }

    /**
     * Substitutions done on every snippet's contents.
     */
    @Input
    Map defaultSubstitutions = [:]

    @TaskAction
    void executeTask() {
        /*
         * Walks each line of each file, building snippets as it encounters
         * the lines that make up the snippet.
         */
        for (File file: docs) {
            String lastLanguage
            int lastLanguageLine
            Snippet snippet = null
            StringBuilder contents = null
            List substitutions = null
            Closure emit = {
                snippet.contents = contents.toString()
                contents = null
                Closure doSubstitution = { String pattern, String subst ->
                    /*
                     * $body is really common but it looks like a
                     * backreference so we just escape it here to make the
                     * tests cleaner.
                     */
                    subst = subst.replace('$body', '\\$body')
                    subst = subst.replace('$_path', '\\$_path')
                    // \n is a new line....
                    subst = subst.replace('\\n', '\n')
                    snippet.contents = snippet.contents.replaceAll(
                        pattern, subst)
                }
                defaultSubstitutions.each doSubstitution
                if (substitutions != null) {
                    substitutions.each doSubstitution
                    substitutions = null
                }
                if (snippet.language == null) {
                    throw new InvalidUserDataException("$snippet: "
                        + "Snippet missing a language. This is required by "
                        + "Elasticsearch's doc testing infrastructure so we "
                        + "be sure we don't accidentally forget to test a "
                        + "snippet.")
                }
                // Try to detect snippets that contain `curl`
                if (snippet.language == 'sh' || snippet.language == 'shell') {
                    snippet.curl = snippet.contents.contains('curl')
                    if (!snippet.console && !snippet.curl) {
                        throw new InvalidUserDataException("$snippet: "
                            + "No need for NOTCONSOLE if snippet doesn't "
                            + "contain `curl`.")
                    }
                }
                if (snippet.testResponse && snippet.language == 'js') {
                    String quoted = snippet.contents
                        // quote values starting with $
                        .replaceAll(/([:,])\s*(\$[^ ,\n}]+)/, '$1 "$2"')
                        // quote fields starting with $
                        .replaceAll(/(\$[^ ,\n}]+)\s*:/, '"$1":')
                    JsonSlurper slurper =
                        new JsonSlurper(type: JsonParserType.INDEX_OVERLAY)
                    try {
                        slurper.parseText(quoted)
                    } catch (JsonException e) {
                        throw new InvalidUserDataException("Invalid json "
                            + "in $snippet. The error is:\n${e.message}.\n"
                            + "After substitutions and munging, the json "
                            + "looks like:\n$quoted", e)
                    }
                }
                perSnippet(snippet)
                snippet = null
            }
            file.eachLine('UTF-8') { String line, int lineNumber ->
                Matcher matcher
                if (line ==~ /-{4,}\s*/) { // Four dashes looks like a snippet
                    if (snippet == null) {
                        Path path = docs.dir.toPath().relativize(file.toPath())
                        snippet = new Snippet(path: path, start: lineNumber)
                        if (lastLanguageLine == lineNumber - 1) {
                            snippet.language = lastLanguage
                        }
                    } else {
                        snippet.end = lineNumber
                    }
                    return
                }
                matcher = line =~ /\["?source"?,\s*"?(\w+)"?(,.*)?].*/
                if (matcher.matches()) {
                    lastLanguage = matcher.group(1)
                    lastLanguageLine = lineNumber
                    return
                }
                if (line ==~ /\/\/\s*AUTOSENSE\s*/) {
                    throw new InvalidUserDataException("$file:$lineNumber: "
                        + "AUTOSENSE has been replaced by CONSOLE.")
                }
                if (line ==~ /\/\/\s*CONSOLE\s*/) {
                    if (snippet == null) {
                        throw new InvalidUserDataException("$file:$lineNumber: "
                            + "CONSOLE not paired with a snippet")
                    }
                    if (snippet.console != null) {
                        throw new InvalidUserDataException("$file:$lineNumber: "
                            + "Can't be both CONSOLE and NOTCONSOLE")
                    }
                    snippet.console = true
                    return
                }
                if (line ==~ /\/\/\s*NOTCONSOLE\s*/) {
                    if (snippet == null) {
                        throw new InvalidUserDataException("$file:$lineNumber: "
                            + "NOTCONSOLE not paired with a snippet")
                    }
                    if (snippet.console != null) {
                        throw new InvalidUserDataException("$file:$lineNumber: "
                            + "Can't be both CONSOLE and NOTCONSOLE")
                    }
                    snippet.console = false
                    return
                }
                matcher = line =~ /\/\/\s*TEST(\[(.+)\])?\s*/
                if (matcher.matches()) {
                    if (snippet == null) {
                        throw new InvalidUserDataException("$file:$lineNumber: "
                            + "TEST not paired with a snippet at ")
                    }
                    snippet.test = true
                    if (matcher.group(2) != null) {
                        String loc = "$file:$lineNumber"
                        parse(loc, matcher.group(2), TEST_SYNTAX) {
                            if (it.group(1) != null) {
                                snippet.catchPart = it.group(1)
                                return
                            }
                            if (it.group(2) != null) {
                                if (substitutions == null) {
                                    substitutions = []
                                }
                                substitutions.add([it.group(2), it.group(3)])
                                return
                            }
                            if (it.group(4) != null) {
                                snippet.skipTest = it.group(4)
                                return
                            }
                            if (it.group(5) != null) {
                                snippet.continued = true
                                return
                            }
                            if (it.group(6) != null) {
                                snippet.setup = it.group(6)
                                return
                            }
                            if (it.group(7) != null) {
                                snippet.warnings.add(it.group(7))
                                return
                            }
                            throw new InvalidUserDataException(
                                    "Invalid test marker: $line")
                        }
                    }
                    return
                }
                matcher = line =~ /\/\/\s*TESTRESPONSE(\[(.+)\])?\s*/
                if (matcher.matches()) {
                    if (snippet == null) {
                        throw new InvalidUserDataException("$file:$lineNumber: "
                            + "TESTRESPONSE not paired with a snippet")
                    }
                    snippet.testResponse = true
                    if (matcher.group(2) != null) {
                        if (substitutions == null) {
                            substitutions = []
                        }
                        String loc = "$file:$lineNumber"
                        parse(loc, matcher.group(2), /(?:$SUBSTITUTION|$CAT) ?/) {
                            if (it.group(1) != null) {
                                // TESTRESPONSE[s/adsf/jkl/]
                                substitutions.add([it.group(1), it.group(2)])
                            } else if (it.group(3) != null) {
                                // TESTRESPONSE[_cat]
                                substitutions.add(['^', '/'])
                                substitutions.add(['\n$', '\\\\s*/'])
                                substitutions.add(['( +)', '$1\\\\s+'])
                                substitutions.add(['\n', '\\\\s*\n '])
                            }
                        }
                    }
                    return
                }
                if (line ==~ /\/\/\s*TESTSETUP\s*/) {
                    snippet.testSetup = true
                    return
                }
                if (snippet == null) {
                    // Outside
                    return
                }
                if (snippet.end == Snippet.NOT_FINISHED) {
                    // Inside
                    if (contents == null) {
                        contents = new StringBuilder()
                    }
                    // We don't need the annotations
                    line = line.replaceAll(/<\d+>/, '')
                    // Nor any trailing spaces
                    line = line.replaceAll(/\s+$/, '')
                    contents.append(line).append('\n')
                    return
                }
                // Just finished
                emit()
            }
            if (snippet != null) emit()
        }
    }

    static class Snippet {
        static final int NOT_FINISHED = -1

        /**
         * Path to the file containing this snippet. Relative to docs.dir of the
         * SnippetsTask that created it.
         */
        Path path
        int start
        int end = NOT_FINISHED
        String contents

        Boolean console = null
        boolean test = false
        boolean testResponse = false
        boolean testSetup = false
        String skipTest = null
        boolean continued = false
        String language = null
        String catchPart = null
        String setup = null
        boolean curl
        List warnings = new ArrayList()

        @Override
        String toString() {
            String result = "$path[$start:$end]"
            if (language != null) {
                result += "($language)"
            }
            if (console != null) {
                result += console ? '// CONSOLE' : '// NOTCONSOLE'
            }
            if (test) {
                result += '// TEST'
                if (catchPart) {
                    result += "[catch: $catchPart]"
                }
                if (skipTest) {
                    result += "[skip=$skipTest]"
                }
                if (continued) {
                    result += '[continued]'
                }
                if (setup) {
                    result += "[setup:$setup]"
                }
                for (String warning in warnings) {
                    result += "[warning:$warning]"
                }
            }
            if (testResponse) {
                result += '// TESTRESPONSE'
            }
            if (testSetup) {
                result += '// TESTSETUP'
            }
            if (curl) {
                result += '(curl)'
            }
            return result
        }
    }

    /**
     * Repeatedly match the pattern to the string, calling the closure with the
     * matchers each time there is a match. If there are characters that don't
     * match then blow up. If the closure takes two parameters then the second
     * one is "is this the last match?".
     */
    protected parse(String location, String s, String pattern, Closure c) {
        if (s == null) {
            return // Silly null, only real stuff gets to match!
        }
        Matcher m = s =~ pattern
        int offset = 0
        Closure extraContent = { message ->
            StringBuilder cutOut = new StringBuilder()
            cutOut.append(s[offset - 6..offset - 1])
            cutOut.append('*')
            cutOut.append(s[offset..Math.min(offset + 5, s.length() - 1)])
            String cutOutNoNl = cutOut.toString().replace('\n', '\\n')
            throw new InvalidUserDataException("$location: Extra content "
                + "$message ('$cutOutNoNl') matching [$pattern]: $s")
        }
        while (m.find()) {
            if (m.start() != offset) {
                extraContent("between [$offset] and [${m.start()}]")
            }
            offset = m.end()
            if (c.maximumNumberOfParameters == 1) {
                c(m)
            } else {
                c(m, offset == s.length())
            }
        }
        if (offset == 0) {
            throw new InvalidUserDataException("$location: Didn't match "
                + "$pattern: $s")
        }
        if (offset != s.length()) {
            extraContent("after [$offset]")
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy