io.cucumber.plugin.event.Node Maven / Gradle / Ivy
Show all versions of cucumber-plugin Show documentation
package io.cucumber.plugin.event;
import org.apiguardian.api.API;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import static java.util.Collections.singletonList;
/**
* A node in a source file.
*
* A node has a location, a keyword and name. The keyword and name are both
* optional (e.g. {@link Example} and blank scenario names).
*
* Nodes are organized in a tree like structure where {@link Container} nodes
* contain yet more nodes.
*
* A node can be linked to a {@link TestCase} by {@link #getLocation()}. The
* {@link Node#findPathTo(Predicate)} method can be used to find a path from the
* root node to a node with the same location as a test case.
*
* {@code Location location = testCase.getLocation();}
* {@code Predicate withLocation = candidate -> location.equals(candidate.getLocation());}
* {@code Optional> path = node.findPathTo(withLocation);}
*
*
*/
@API(status = API.Status.EXPERIMENTAL)
public interface Node {
Location getLocation();
Optional getKeyword();
Optional getName();
Optional getParent();
/**
* Recursively maps a node into another tree-like structure.
*
* @param parent the parent node of the target structure
* @param mapFeature a function that takes a feature and a parent
* node and returns a mapped feature
* @param mapRule a function that takes a rule and a parent node
* and returns a mapped rule
* @param mapScenario a function that takes a scenario and a parent
* node and returns a mapped scenario
* @param mapScenarioOutline a function that takes a scenario outline and a
* parent node and returns a mapped scenario
* outline
* @param mapExamples a function that takes an examples and a parent
* node and returns a mapped examples
* @param mapExample a function that takes an example and a parent
* node and returns a mapped example
* @param the type of the target structure
* @return the mapped version of this instance
*/
default T map(
T parent,
BiFunction mapFeature,
BiFunction mapRule, BiFunction mapScenario,
BiFunction mapScenarioOutline,
BiFunction mapExamples,
BiFunction mapExample
) {
if (this instanceof Scenario) {
return mapScenario.apply((Scenario) this, parent);
} else if (this instanceof Example) {
return mapExample.apply((Example) this, parent);
} else if (this instanceof Container) {
final T mapped;
if (this instanceof Feature) {
mapped = mapFeature.apply((Feature) this, parent);
} else if (this instanceof Rule) {
mapped = mapRule.apply((Rule) this, parent);
} else if (this instanceof ScenarioOutline) {
mapped = mapScenarioOutline.apply((ScenarioOutline) this, parent);
} else if (this instanceof Examples) {
mapped = mapExamples.apply((Examples) this, parent);
} else {
throw new IllegalArgumentException(this.getClass().getName());
}
Container> container = (Container>) this;
container.elements().forEach(node -> node.map(mapped, mapFeature, mapRule, mapScenario, mapScenarioOutline,
mapExamples, mapExample));
return mapped;
} else {
throw new IllegalArgumentException(this.getClass().getName());
}
}
/**
* Finds a path down tree starting at this node to the first node that
* matches the predicate using depth first search.
*
* @param predicate to match the target node.
* @return a path to the first node or an empty optional if none
* was found.
*/
default Optional> findPathTo(Predicate predicate) {
if (predicate.test(this)) {
List path = new ArrayList<>();
path.add(this);
return Optional.of(path);
}
return Optional.empty();
}
interface Container extends Node {
@Override
default Optional> findPathTo(Predicate predicate) {
List path = new ArrayList<>();
Deque> toSearch = new ArrayDeque<>();
toSearch.addLast(new ArrayDeque<>(singletonList(this)));
while (!toSearch.isEmpty()) {
Deque candidates = toSearch.peekLast();
if (candidates.isEmpty()) {
if (!path.isEmpty()) {
path.remove(path.size() - 1);
}
toSearch.removeLast();
continue;
}
Node candidate = candidates.pop();
if (predicate.test(candidate)) {
path.add(candidate);
return Optional.of(path);
}
if (candidate instanceof Container) {
path.add(candidate);
Container> container = (Container>) candidate;
toSearch.addLast(new ArrayDeque<>(container.elements()));
}
}
return Optional.empty();
}
Collection elements();
}
/**
* A feature has a keyword and optionally a name.
*/
interface Feature extends Node, Container {
}
/**
* A rule has a keyword and optionally a name.
*/
interface Rule extends Node, Container {
}
/**
* A scenario has a keyword and optionally a name.
*/
interface Scenario extends Node {
}
/**
* A scenario outline has a keyword and optionally a name.
*/
interface ScenarioOutline extends Node, Container {
}
/**
* An examples section has a keyword and optionally a name.
*/
interface Examples extends Node, Container {
}
/**
* An example has no keyword but always a name.
*/
interface Example extends Node {
}
}