Skip to content

Commit

Permalink
Add Anchor and Alias support to the Yaml model and JsonPathMatcher (o…
Browse files Browse the repository at this point in the history
…penrewrite#2272)

Yaml Anchor / Alias support

- Add a new `EntryKey` interface for `Mapping.Entry.key` values which are either scalar or alias blocks.
- Update `YamlParser` to handle `Mapping.Entry.key` as an instance of `EntryKey`
- New `ReplaceAliasWithAnchorValueVisitor`.
- Update `JsonPathMatcher` to replace alias trees with their associated anchor value before searching.
- Update `CoalesceProperties` to prevent transforming documents having anchor/alias pairs.
- Update `org.openrewrite.yaml.DeleteProperty` to prevent deleting properties from documents having anchor/alias pars.
  • Loading branch information
pway99 authored Sep 28, 2022
1 parent 7a0bdce commit 2703681
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ public YamlVisitor<ExecutionContext> getVisitor() {
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext context) {
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, context);
if (matcher.matches(getCursor())) {
e = e.withKey(e.getKey().withValue(newKey));
if (e.getKey() instanceof Yaml.Scalar) {
e = e.withKey(((Yaml.Scalar)e.getKey()).withValue(newKey));
}
}
return e;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public CoalescePropertiesVisitor() {

@Override
public Yaml.Document visitDocument(Yaml.Document document, P p) {
if (document != new ReplaceAliasWithAnchorValueVisitor<P>().visit(document, p)) {
return document;
}
findIndent.visit(document, p);
return super.visitDocument(document, p);
}
Expand All @@ -47,7 +50,7 @@ public Yaml.Mapping visitMapping(Yaml.Mapping mapping, P p) {
if (valueMapping.getEntries().size() == 1) {
Yaml.Mapping.Entry subEntry = valueMapping.getEntries().iterator().next();
if (!subEntry.getPrefix().contains("#")) {
Yaml.Scalar coalescedKey = entry.getKey().withValue(entry.getKey().getValue() + "." + subEntry.getKey().getValue());
Yaml.Scalar coalescedKey = ((Yaml.Scalar) entry.getKey()).withValue(entry.getKey().getValue() + "." + subEntry.getKey().getValue());

entries.add(entry.withKey(coalescedKey)
.withValue(subEntry.getValue()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
@Override
public YamlVisitor<ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {

@Override
public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext executionContext) {
// TODO: Update DeleteProperty to support documents having Anchor / Alias Pairs
if (documents != new ReplaceAliasWithAnchorValueVisitor<ExecutionContext>().visit(documents, executionContext)) {
return documents;
}
return super.visitDocuments(documents, executionContext);
}

@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public <T> Optional<T> find(Cursor cursor) {
LinkedList<Tree> cursorPath = cursor.getPathAsStream()
.filter(o -> o instanceof Tree)
.map(Tree.class::cast)
.map(t -> new ReplaceAliasWithAnchorValueVisitor<Integer>()
.visitNonNull(t, 0))
.collect(Collectors.toCollection(LinkedList::new));
if (cursorPath.isEmpty()) {
return Optional.empty();
Expand All @@ -78,14 +80,22 @@ public <T> Optional<T> find(Cursor cursor) {
}

public boolean matches(Cursor cursor) {
List<Object> cursorPath = cursor.getPathAsStream().collect(Collectors.toList());
Object cursorValue = new ReplaceAliasWithAnchorValueVisitor<Integer>().visit((Tree)cursor.getValue(), 0);
List<Object> cursorPath = cursor.getPathAsStream()
.map(cp -> {
if (cp instanceof Yaml) {
cp = new ReplaceAliasWithAnchorValueVisitor<Integer>().visit((Yaml) cp, 0);
}
return cp;
})
.collect(Collectors.toList());
return find(cursor).map(o -> {
if (o instanceof List) {
//noinspection unchecked
List<Object> l = (List<Object>) o;
return !disjoint(l, cursorPath) && l.contains(cursor.getValue());
return !disjoint(l, cursorPath) && l.contains(cursorValue);
} else {
return Objects.equals(o, cursor.getValue());
return Objects.equals(o, cursorValue);
}
}).orElse(false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2022 the original author or authors.
* <p>
* Licensed 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite.yaml;

import org.openrewrite.yaml.tree.Yaml;

import java.util.HashMap;
import java.util.Map;

public class ReplaceAliasWithAnchorValueVisitor<P> extends YamlVisitor<P> {
Map<String, Yaml> anchorValues = new HashMap<>();
@Override
public Yaml visitMapping(Yaml.Mapping mapping, P p) {
if (mapping.getAnchor() != null) {
anchorValues.put(mapping.getAnchor().getKey(), mapping.withAnchor(null));
}
return super.visitMapping(mapping, p);
}

@Override
public Yaml visitScalar(Yaml.Scalar scalar, P p) {
if (scalar.getAnchor() != null) {
anchorValues.put(scalar.getAnchor().getKey(), scalar.withAnchor(null));
}
return super.visitScalar(scalar, p);
}

@Override
public Yaml visitAlias(Yaml.Alias alias, P p) {
Yaml.Alias al = (Yaml.Alias) super.visitAlias(alias, p);
Yaml anchorVal = anchorValues.get(al.getAnchor().getKey());
if (anchorVal != null) {
return anchorVal;
}
return al;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.openrewrite.tree.ParsingEventListener;
import org.openrewrite.tree.ParsingExecutionContextView;
import org.openrewrite.yaml.tree.Yaml;
import org.openrewrite.yaml.tree.YamlKey;
import org.yaml.snakeyaml.events.*;
import org.yaml.snakeyaml.parser.Parser;
import org.yaml.snakeyaml.parser.ParserImpl;
Expand Down Expand Up @@ -418,7 +419,7 @@ private static class MappingBuilder implements BlockBuilder {
private final List<Yaml.Mapping.Entry> entries = new ArrayList<>();

@Nullable
private Yaml.Scalar key;
private YamlKey key;

private MappingBuilder(String prefix, @Nullable String startBracePrefix, @Nullable Yaml.Anchor anchor) {
this.prefix = prefix;
Expand All @@ -430,6 +431,8 @@ private MappingBuilder(String prefix, @Nullable String startBracePrefix, @Nullab
public void push(Yaml.Block block) {
if (key == null && block instanceof Yaml.Scalar) {
key = (Yaml.Scalar) block;
} else if (key == null && block instanceof Yaml.Alias) {
key = (Yaml.Alias) block;
} else {
String keySuffix = block.getPrefix();
block = block.withPrefix(keySuffix.substring(commentAwareIndexOf(':', keySuffix) + 1));
Expand Down
11 changes: 8 additions & 3 deletions rewrite-yaml/src/main/java/org/openrewrite/yaml/tree/Yaml.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public End copyPaste() {
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Scalar implements Block {
class Scalar implements Block, YamlKey {
@EqualsAndHashCode.Include
UUID id;

Expand Down Expand Up @@ -288,7 +288,7 @@ public static class Entry implements Yaml {

String prefix;
Markers markers;
Scalar key;
YamlKey key;

// https://yaml.org/spec/1.2/spec.html#:%20mapping%20value//
String beforeMappingValueIndicator;
Expand Down Expand Up @@ -411,7 +411,7 @@ public Entry copyPaste() {
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Alias implements Block {
class Alias implements Block, YamlKey {
@EqualsAndHashCode.Include
UUID id;

Expand All @@ -429,6 +429,11 @@ public <P> Yaml acceptYaml(YamlVisitor<P> v, P p) {
return v.visitAlias(this, p);
}

@Override
public String getValue() {
return anchor.key;
}

@Override
public Alias copyPaste() {
return new Alias(randomId(), prefix, Markers.EMPTY, anchor);
Expand Down
26 changes: 26 additions & 0 deletions rewrite-yaml/src/main/java/org/openrewrite/yaml/tree/YamlKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2022 the original author or authors.
* <p>
* Licensed 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite.yaml.tree;

/**
* Yaml Keys are either Scalar or Alias blocks
*/
public interface YamlKey extends Yaml {
String getValue();
YamlKey copyPaste();

YamlKey withPrefix(String prefix);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2022 the original author or authors.
* <p>
* Licensed 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite.yaml;

import org.junit.jupiter.api.Test;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.yaml.Assertions.yaml;

class ReplaceAliasWithAnchorValueTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(RewriteTest.toRecipe(ReplaceAliasWithAnchorValueVisitor::new));
}

@Test
void simpleCase() {
rewriteRun(
yaml("""
bar:
&abc yo: friend
baz:
*abc: friendly
""",
"""
bar:
&abc yo: friend
baz:
yo: friendly
"""
)
);
}

@Test
void aliasRefersToLastKnownAnchorValue() {
rewriteRun(
yaml("""
definitions:
steps:
- step: &build-test
name: Build and test
- step2: &build-deploy
name: Build and Deploy
pipelines:
branches:
develop:
- step: *build-test
master:
- step: *build-deploy
environments:
branches:
test:
- step: &build-test
name: ReUsed Build and Test
qa:
- step: *build-test
""",
"""
definitions:
steps:
- step: &build-test
name: Build and test
- step2: &build-deploy
name: Build and Deploy
pipelines:
branches:
develop:
- step:
name: Build and test
master:
- step:
name: Build and Deploy
environments:
branches:
test:
- step: &build-test
name: ReUsed Build and Test
qa:
- step:
name: ReUsed Build and Test
"""
)
);
}

@Test
void howAboutSequences() {
rewriteRun(
yaml("""
stages:
- id: &id ping_api_endpoint
name: *id
- id: &id get_token
name: *id
- id: &id do_something
name: *id
- id: &id revoke_token
name: *id
""",
"""
stages:
- id: &id ping_api_endpoint
name: ping_api_endpoint
- id: &id get_token
name: get_token
- id: &id do_something
name: do_something
- id: &id revoke_token
name: revoke_token
"""
)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ class ChangeValueTest : YamlRecipeTest {
"""
)

@Test
fun changeAliasedKeyValue() = assertChanged(
recipe = ChangeValue(
"$.*.yo",
"howdy",
null
),
before = """
bar:
&abc yo: friend
baz:
*abc: friendly
""",
after = """
bar:
&abc yo: howdy
baz:
*abc: howdy
"""
)

@Test
fun changeSequenceValue() = assertChanged(
recipe = ChangeValue(
Expand Down
Loading

0 comments on commit 2703681

Please sign in to comment.