Skip to content

Commit

Permalink
added by modulator
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentyn Kahamlyk authored and Valentyn Kahamlyk committed Oct 24, 2023
1 parent 87eb798 commit 39e3897
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.tinkerpop.gremlin.process.traversal.Pop;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.step.ByModulating;
import org.apache.tinkerpop.gremlin.process.traversal.step.PathProcessor;
import org.apache.tinkerpop.gremlin.process.traversal.step.Scoping;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
Expand All @@ -33,10 +34,8 @@
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
Expand All @@ -49,9 +48,12 @@
*
* @author Valentyn Kahamlyk
*/
public final class FormatStep<S> extends MapStep<S, String> implements TraversalParent, Scoping, PathProcessor {
public final class FormatStep<S> extends MapStep<S, String> implements ByModulating, TraversalParent, Scoping, PathProcessor {

private static final Pattern VARIABLE_PATTERN = Pattern.compile("%\\{(.*?)\\}");
private static final String FROM_BY = "_";
// first matching group is start of string OR NOT %
// second group is variable name
private static final Pattern VARIABLE_PATTERN = Pattern.compile("(^|[^%])%\\{(.*?)\\}");

private String format;
private Set<String> variables;
Expand All @@ -66,36 +68,47 @@ public FormatStep(final Traversal.Admin traversal, final String format) {

@Override
protected Traverser.Admin<String> processNextStart() {
final Map<String, Object> values = new HashMap<>();

final Traverser.Admin traverser = this.starts.next();

boolean productive = true;
for (final String var : variables) {
final Object current = traverser.get();
// try to get property value
if (current instanceof Element) {
final Property prop = ((Element) current).property(var);
int lastIndex = 0;
final StringBuilder output = new StringBuilder();
final Matcher matcher = VARIABLE_PATTERN.matcher(format);
final Object current = traverser.get();

while (matcher.find()) {
final String varName = matcher.group(2);
final int start = matcher.start() + matcher.group(1).length();
if (varName == null) continue;

if (!varName.equals(FROM_BY) && current instanceof Element) {
final Property prop = ((Element) current).property(varName);
if (prop != null && prop.isPresent()) {
values.put(var, prop.value());
output.append(format, lastIndex, start).append(prop.value());
lastIndex = matcher.end();
continue;
}
}

final TraversalProduct product =
TraversalUtil.produce((S) this.getNullableScopeValue(Pop.last, var, traverser), this.traversalRing.next());
final TraversalProduct product = varName.equals(FROM_BY) ?
TraversalUtil.produce(traverser, this.traversalRing.next()) :
TraversalUtil.produce((S) this.getNullableScopeValue(Pop.last, varName, traverser), this.traversalRing.next());

if (!product.isProductive() || product.get() == null) {
productive = false;
break;
}

values.put(var, product.get());
output.append(format, lastIndex, start).append(product.get());
lastIndex = matcher.end();
}

if (lastIndex < format.length()) {
output.append(format, lastIndex, format.length());
}
this.traversalRing.reset();

return productive ?
PathProcessor.processTraverserPathLabels(traverser.split(evaluate(values), this), this.keepLabels) :
PathProcessor.processTraverserPathLabels(traverser.split(output.toString(), this), this.keepLabels) :
EmptyTraverser.instance();
}

Expand Down Expand Up @@ -156,39 +169,29 @@ public Set<String> getKeepLabels() {
return this.keepLabels;
}

@Override
public void modulateBy(final Traversal.Admin<?, ?> selectTraversal) {
this.traversalRing.addTraversal(this.integrateChild(selectTraversal));
}

@Override
public void replaceLocalChild(final Traversal.Admin<?, ?> oldTraversal, final Traversal.Admin<?, ?> newTraversal) {
this.traversalRing.replaceTraversal(
(Traversal.Admin<S, String>) oldTraversal,
(Traversal.Admin<S, String>) newTraversal);
}

// private methods

private Set<String> getVariables() {
final Matcher matcher = VARIABLE_PATTERN.matcher(format);
final Set<String> variables = new LinkedHashSet<>();
while (matcher.find()) {
final String varName = matcher.group(1);
if (varName != null) {
variables.add(matcher.group(1));
final String varName = matcher.group(2);
if (varName != null && !varName.equals(FROM_BY)) {
variables.add(varName);
}
}
return variables;
}

private String evaluate(final Map<String, Object> values) {
int lastIndex = 0;
final StringBuilder output = new StringBuilder();
final Matcher matcher = VARIABLE_PATTERN.matcher(format);
matcher.reset();

while (matcher.find()) {
final String varName = matcher.group(1);
if (varName == null) continue;

final Object value = values.get(varName);
output.append(format, lastIndex, matcher.start()).append(value);

lastIndex = matcher.end();
}

if (lastIndex < format.length()) {
output.append(format, lastIndex, format.length());
}
return output.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.StepTest;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty;
import org.junit.Test;

import java.util.ArrayList;
Expand All @@ -32,8 +33,6 @@

import static org.apache.tinkerpop.gremlin.util.CollectionUtil.asMap;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class FormatStepTest extends StepTest {

Expand All @@ -55,9 +54,12 @@ public void shouldGetVariablesFromTemplate() {
assertEquals(Collections.emptyList(), getVariables("Hello %{world"));
assertEquals(Collections.emptyList(), getVariables("Hello {world}"));
assertEquals(Collections.emptyList(), getVariables("Hello % {world}"));
assertEquals(Collections.emptyList(), getVariables("Hello %%{world}"));
assertEquals(Collections.emptyList(), getVariables("Hello %{_}"));
assertEquals(Collections.singletonList(""), getVariables("Hello %{}"));
assertEquals(Collections.singletonList(" "), getVariables("Hello %{ }"));
assertEquals(Collections.singletonList("world"), getVariables("Hello %{world}"));
assertEquals(Collections.singletonList("world"), getVariables("%%{Hello} %{world} %{_}"));
assertEquals(Arrays.asList("Hello", "world"), getVariables("%{Hello} %{world}"));
assertEquals(Arrays.asList("Hello", "world"), getVariables("%{Hello} %{Hello} %{world}"));
assertEquals(Arrays.asList("Hello", "hello", "world"), getVariables("%{Hello} %{hello} %{world}"));
Expand All @@ -70,38 +72,36 @@ public void shouldWorkWithoutVariables() {

@Test
public void shouldWorkWithVertexInput() {
final VertexProperty mockProperty = mock(VertexProperty.class);
when(mockProperty.key()).thenReturn("name");
when(mockProperty.value()).thenReturn("Stephen");
when(mockProperty.isPresent()).thenReturn(true);
final Vertex vertex1 = new DetachedVertex(10L, "person", Collections.singletonList(
DetachedVertexProperty.build().setId(1).setLabel("name").setValue("Stephen").create()));

final Vertex mockVertex = mock(Vertex.class);
when(mockVertex.property("name")).thenReturn(mockProperty);

assertEquals("Hello Stephen", __.__(mockVertex).format("Hello %{name}").next());
assertEquals("Hello Stephen", __.__(vertex1).format("Hello %{name}").next());
}

@Test
public void shouldWorkWithMultipleVertexInput() {
final VertexProperty mockProperty1 = mock(VertexProperty.class);
when(mockProperty1.key()).thenReturn("name");
when(mockProperty1.value()).thenReturn("Stephen");
when(mockProperty1.isPresent()).thenReturn(true);
final Vertex vertex1 = new DetachedVertex(10L, "person", Collections.singletonList(
DetachedVertexProperty.build().setId(1).setLabel("name").setValue("Stephen").create()));

final Vertex mockVertex1 = mock(Vertex.class);
when(mockVertex1.property("name")).thenReturn(mockProperty1);
final Vertex vertex2 = new DetachedVertex(11L, "person", Collections.singletonList(
DetachedVertexProperty.build().setId(1).setLabel("name").setValue("Marko").create()));

final VertexProperty mockProperty2 = mock(VertexProperty.class);
when(mockProperty2.key()).thenReturn("name");
when(mockProperty2.value()).thenReturn("Marko");
when(mockProperty2.isPresent()).thenReturn(true);
assertEquals(Arrays.asList("Hello Stephen", "Hello Marko"),
__.__(vertex1, vertex2).format("Hello %{name}").toList());
}

@Test
public void shouldWorkWithModulator() {
final Vertex vertex1 = new DetachedVertex(10L, "person", Collections.singletonList(
DetachedVertexProperty.build().setId(1).setLabel("name").setValue("Stephen").create()));

final Vertex mockVertex2 = mock(Vertex.class);
when(mockVertex2.property("name")).thenReturn(mockProperty2);
final Vertex vertex2 = new DetachedVertex(11L, "person", Collections.singletonList(
DetachedVertexProperty.build().setId(1).setLabel("name").setValue("Marko").create()));

assertEquals(Arrays.asList("Hello Stephen", "Hello Marko"),
__.__(mockVertex1, mockVertex2).format("Hello %{name}").toList());
__.__(vertex1, vertex2).format("%{_} %{_}").by(__.constant("Hello")).by(__.values("name")).toList());
}

@Test
public void shouldWorkWithMap() {
assertEquals("Hello 2", __.__(asMap("name", 2)).format("Hello %{name}").next());
Expand All @@ -121,38 +121,23 @@ public void shouldHandleSameVariableTwice() {

@Test
public void shouldWorkWithMixedInput() {
final VertexProperty mockProperty1 = mock(VertexProperty.class);
when(mockProperty1.key()).thenReturn("p1");
when(mockProperty1.value()).thenReturn("val1");
when(mockProperty1.isPresent()).thenReturn(true);

final VertexProperty mockProperty2 = mock(VertexProperty.class);
when(mockProperty2.key()).thenReturn("p2");
when(mockProperty2.value()).thenReturn("val2");
when(mockProperty2.isPresent()).thenReturn(true);

final Vertex mockVertex = mock(Vertex.class);
when(mockVertex.property("p1")).thenReturn(mockProperty1);
when(mockVertex.property("p2")).thenReturn(mockProperty2);
final Vertex vertex = new DetachedVertex(10L, "test", Arrays.asList(
DetachedVertexProperty.build().setId(1).setLabel("p1").setValue("val1").create(),
DetachedVertexProperty.build().setId(2).setLabel("p2").setValue("val2").create()));

assertEquals("val1 val2 valA valB",
__.inject("valA").as("varA").
constant("valB").as("varB").
constant(mockVertex).format("%{p1} %{p2} %{varA} %{varB}").next());
constant(vertex).format("%{p1} %{p2} %{varA} %{varB}").next());
}

@Test
public void shouldPrioritizeVertexPropertiesOverScopeVariables() {
final VertexProperty mockProperty1 = mock(VertexProperty.class);
when(mockProperty1.key()).thenReturn("name");
when(mockProperty1.value()).thenReturn("Stephen");
when(mockProperty1.isPresent()).thenReturn(true);

final Vertex mockVertex = mock(Vertex.class);
when(mockVertex.property("name")).thenReturn(mockProperty1);
final Vertex vertex = new DetachedVertex(10L, "person", Collections.singletonList(
DetachedVertexProperty.build().setId(1).setLabel("name").setValue("Stephen").create()));

assertEquals("Hello Stephen",
__.__("Marko").as("name").
constant(mockVertex).format("Hello %{name}").next());
constant(vertex).format("Hello %{name}").next());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ Feature: Step - format()
| josh is 32 years old |
| peter is 35 years old |

Scenario: g_V_formatXstrX_byXnameX_byXageX
Given the modern graph
And the traversal of
"""
g.V().format("%{_} is %{_} years old").by(__.values("name")).by(__.values("age"))
"""
When iterated to list
# software don't have age, so filtered out
Then the result should be unordered
| result |
| marko is 29 years old |
| vadas is 27 years old |
| josh is 32 years old |
| peter is 35 years old |

Scenario: g_V_elementMap_formatXstrX
Given the modern graph
And the traversal of
Expand Down

0 comments on commit 39e3897

Please sign in to comment.