[
https://issues.apache.org/jira/browse/TINKERPOP-2334?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17779183#comment-17779183
]
ASF GitHub Bot commented on TINKERPOP-2334:
-------------------------------------------
Cole-Greer commented on code in PR #2307:
URL: https://github.com/apache/tinkerpop/pull/2307#discussion_r1370523821
##########
gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/FormatStep.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import org.apache.commons.lang3.StringUtils;
+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.PathProcessor;
+import org.apache.tinkerpop.gremlin.process.traversal.step.Scoping;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
+import
org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
+import
org.apache.tinkerpop.gremlin.process.traversal.traverser.util.EmptyTraverser;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalProduct;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalRing;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
+import org.apache.tinkerpop.gremlin.structure.Element;
+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;
+import java.util.regex.Pattern;
+
+/**
+ * Reference implementation for String format step, a mid-traversal step which
will handle result formatting
+ * to string values. If the incoming traverser is a non-String value then an
{@code IllegalArgumentException}
+ * will be thrown.
+ *
+ * @author Valentyn Kahamlyk
+ */
+public final class FormatStep<S> extends MapStep<S, String> implements
TraversalParent, Scoping, PathProcessor {
+
+ private String format;
+ private Set<String> variables;
+ private TraversalRing<S, String> traversalRing = new TraversalRing<>();
+ private Set<String> keepLabels;
+
+ public FormatStep(final Traversal.Admin traversal, final String format) {
+ super(traversal);
+ this.format = format;
+ this.variables = getVariables();
+ }
+
+ @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);
+ if (prop != null && prop.isPresent()) {
+ values.put(var, prop.value());
+ continue;
+ }
+ }
+
+ final TraversalProduct product =
+ TraversalUtil.produce((S)
this.getNullableScopeValue(Pop.last, var, traverser),
this.traversalRing.next());
+
+ if (!product.isProductive() || product.get() == null) {
+ productive = false;
+ break;
+ }
+
+ values.put(var, product.get());
+ }
+ this.traversalRing.reset();
+
+ return productive ?
+
PathProcessor.processTraverserPathLabels(traverser.split(evaluate(values),
this), this.keepLabels) :
+ EmptyTraverser.instance();
+ }
+
+ @Override
+ public String toString() {
+ return StringFactory.stepString(this, this.format, this.traversalRing);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ return Objects.hash(result, format, traversalRing);
+ }
+
+ @Override
+ public List<Traversal.Admin<S, String>> getLocalChildren() {
+ return this.traversalRing.getTraversals();
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ this.traversalRing.reset();
+ }
+
+ @Override
+ public FormatStep<S> clone() {
+ final FormatStep<S> clone = (FormatStep<S>) super.clone();
+ clone.format = this.format;
+ clone.variables = this.variables;
+ clone.traversalRing = this.traversalRing;
+ return clone;
+ }
+
+ @Override
+ public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+ super.setTraversal(parentTraversal);
+ this.traversalRing.getTraversals().forEach(this::integrateChild);
+ }
+
+ @Override
+ public Set<TraverserRequirement> getRequirements() {
+ return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT,
TraverserRequirement.SIDE_EFFECTS);
+ }
+
+ @Override
+ public Set<String> getScopeKeys() {
+ return variables;
+ }
+
+ @Override
+ public void setKeepLabels(final Set<String> labels) {
+ this.keepLabels = labels;
+ }
+
+ @Override
+ public Set<String> getKeepLabels() {
+ return this.keepLabels;
+ }
+
+ // private methods
+
+ private static final Pattern VARIABLE_PATTERN =
Pattern.compile("%\\{(.*?)\\}");
+
+ 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 (!StringUtils.isEmpty(varName)) {
+ variables.add(matcher.group(1));
+ }
+ }
+ 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 (StringUtils.isEmpty(varName)) continue;
Review Comment:
I think an empty string should be a valid varName based on this:
```
gremlin> g.inject("test").as("").V(1).select("")
==>test
```
##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/FormatStepTest.java:
##########
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+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.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+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 {
+
+ @Override
+ protected List<Traversal> getTraversals() {
+ return Collections.singletonList(
+ __.format("hello")
+ );
+ }
+
+ private List<String> getVariables(final String format) {
+ final FormatStep formatStep = new
FormatStep(__.inject("test").asAdmin(), format);
+ return new ArrayList<>(formatStep.getScopeKeys());
+ }
+
+ @Test
+ public void shouldGetVariablesFromTemplate() {
+ assertEquals(Collections.emptyList(), getVariables("Hello world"));
+ assertEquals(Collections.emptyList(), getVariables("Hello %{world"));
+ assertEquals(Collections.emptyList(), getVariables("Hello %{}"));
+ assertEquals(Collections.emptyList(), getVariables("Hello {world}"));
+ assertEquals(Collections.emptyList(), getVariables("Hello % {world}"));
+ assertEquals(Collections.singletonList(" "), getVariables("Hello %{
}"));
+ 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}"));
+ }
+
+ @Test
+ public void shouldWorkWithoutVariables() {
+ assertEquals("Hello world", __.__("test").format("Hello
world").next());
+ }
+
+ @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 mockVertex = mock(Vertex.class);
+ when(mockVertex.property("name")).thenReturn(mockProperty);
+
+ assertEquals("Hello Stephen", __.__(mockVertex).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 mockVertex1 = mock(Vertex.class);
+ when(mockVertex1.property("name")).thenReturn(mockProperty1);
+
+ final VertexProperty mockProperty2 = mock(VertexProperty.class);
+ when(mockProperty2.key()).thenReturn("name");
+ when(mockProperty2.value()).thenReturn("Marko");
+ when(mockProperty2.isPresent()).thenReturn(true);
+
+ final Vertex mockVertex2 = mock(Vertex.class);
+ when(mockVertex2.property("name")).thenReturn(mockProperty2);
+
+ assertEquals(Arrays.asList("Hello Stephen", "Hello Marko"),
+ __.__(mockVertex1, mockVertex2).format("Hello
%{name}").toList());
+ }
+ @Test
+ public void shouldWorkWithMap() {
+ assertEquals("Hello 2", __.__(asMap("name", 2)).format("Hello
%{name}").next());
+ assertEquals("Hello Stephen", __.__(asMap("name",
"Stephen")).format("Hello %{name}").next());
+ }
+
+ @Test
+ public void shouldWorkWithScopeVariables() {
+ assertEquals("Hello Stephen",
__.__("Stephen").as("name").format("Hello %{name}").next());
+ }
+
+ @Test
+ public void shouldHandleSameVariableTwice() {
+ assertEquals("Hello, Hello Stephen",
+ __.__("Hello").as("action").format("%{action}, %{action}
Stephen").next());
+ }
+
+ @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);
+
+ assertEquals("val1 val2 valA valB",
+ __.inject("valA").as("varA").
+ constant("valB").as("varB").
+ constant(mockVertex).format("%{p1} %{p2} %{varA}
%{varB}").next());
+ }
Review Comment:
I'm a bit concerned about namespace collisions between property keys and
scope variables. Could you add a test which has a name collision? Based on the
semantics docs, I would expect the following:
```
gremlin> g.inject("stephen").as("name").V(1).format("Hello %{name}")
==>Hello marko
```
##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/FormatStepTest.java:
##########
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+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.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+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 {
+
+ @Override
+ protected List<Traversal> getTraversals() {
+ return Collections.singletonList(
+ __.format("hello")
+ );
+ }
+
+ private List<String> getVariables(final String format) {
+ final FormatStep formatStep = new
FormatStep(__.inject("test").asAdmin(), format);
+ return new ArrayList<>(formatStep.getScopeKeys());
+ }
+
+ @Test
+ public void shouldGetVariablesFromTemplate() {
+ assertEquals(Collections.emptyList(), getVariables("Hello world"));
+ assertEquals(Collections.emptyList(), getVariables("Hello %{world"));
+ assertEquals(Collections.emptyList(), getVariables("Hello %{}"));
+ assertEquals(Collections.emptyList(), getVariables("Hello {world}"));
+ assertEquals(Collections.emptyList(), getVariables("Hello % {world}"));
+ assertEquals(Collections.singletonList(" "), getVariables("Hello %{
}"));
+ 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}"));
+ }
+
+ @Test
+ public void shouldWorkWithoutVariables() {
+ assertEquals("Hello world", __.__("test").format("Hello
world").next());
+ }
+
+ @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 mockVertex = mock(Vertex.class);
+ when(mockVertex.property("name")).thenReturn(mockProperty);
+
+ assertEquals("Hello Stephen", __.__(mockVertex).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 mockVertex1 = mock(Vertex.class);
+ when(mockVertex1.property("name")).thenReturn(mockProperty1);
+
+ final VertexProperty mockProperty2 = mock(VertexProperty.class);
+ when(mockProperty2.key()).thenReturn("name");
+ when(mockProperty2.value()).thenReturn("Marko");
+ when(mockProperty2.isPresent()).thenReturn(true);
+
+ final Vertex mockVertex2 = mock(Vertex.class);
+ when(mockVertex2.property("name")).thenReturn(mockProperty2);
+
+ assertEquals(Arrays.asList("Hello Stephen", "Hello Marko"),
+ __.__(mockVertex1, mockVertex2).format("Hello
%{name}").toList());
+ }
+ @Test
+ public void shouldWorkWithMap() {
+ assertEquals("Hello 2", __.__(asMap("name", 2)).format("Hello
%{name}").next());
+ assertEquals("Hello Stephen", __.__(asMap("name",
"Stephen")).format("Hello %{name}").next());
+ }
+
+ @Test
+ public void shouldWorkWithScopeVariables() {
+ assertEquals("Hello Stephen",
__.__("Stephen").as("name").format("Hello %{name}").next());
+ }
+
+ @Test
+ public void shouldHandleSameVariableTwice() {
+ assertEquals("Hello, Hello Stephen",
+ __.__("Hello").as("action").format("%{action}, %{action}
Stephen").next());
+ }
+
Review Comment:
Could you add a test for undefined variables being filtered out?
```
gremlin> g.V(1).format("%{name}'s birthday is %{birthday}!")
//No results
```
##########
docs/src/dev/provider/gremlin-semantics.asciidoc:
##########
@@ -892,6 +892,36 @@ None
None
+[[format-step]]
+=== format()
+
+*Description:* a mid-traversal step which will handle result formatting to
string values.
+
+*Syntax:* `format(String)`
+
+[width="100%",options="header"]
+|=========================================================
+|Start Step |Mid Step |Modulated |Domain |Range
+|N |Y |N |`any` |`String`
+|=========================================================
+
+*Arguments:*
+
+Format string. Variables can be represented using `%{variable_name}` notation.
+The variable values are used in the order that the first one will be found:
Element properties, than Scope values.
+If value for some variable was not found than result for input is skipped.
Review Comment:
I prefer `filtered` over `skipped` here. To me, skipped is ambiguous if it
means the whole string result is skipped, or if that single variable is left
blank in the result.
```suggestion
The variable values are used in the order that the first one will be found:
Element properties, then Scope values.
If value for some variable was not found, then the result is filtered out.
```
> Add format() step
> -----------------
>
> Key: TINKERPOP-2334
> URL: https://issues.apache.org/jira/browse/TINKERPOP-2334
> Project: TinkerPop
> Issue Type: Improvement
> Components: process
> Affects Versions: 3.4.4
> Reporter: Stephen Mallette
> Assignee: Valentyn Kahamlyk
> Priority: Major
>
> Provide for a {{format()}} step which will handle result formatting to string
> values. This change will help resolve the need for string concatenation
> functions while providing a lot of flexibility to how results can be formed:
> {code}
> gremlin> g.V().hasLabel('person').format("%{n} is %{a} years old.").by('n',
> 'name').by('a', 'age')
> ==>marko is 29 years old.
> ==>vadas is 27 years old.
> ==>josh is 32 years old.
> ==>peter is 35 years old.
> gremlin> g.V().hasLabel('person').format("%{name} is %{age} years old.")
> ==>marko is 29 years old.
> ==>vadas is 27 years old.
> ==>josh is 32 years old.
> ==>peter is 35 years old.
> {code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)