Skip to content

Commit 9918b78

Browse files
committed
Add ChangeYamlPropertyConditionally recipe and move to yaml package
- Add new recipe to update YAML properties conditionally based on another property's value in the same document - Move all YAML recipes to com.anacoders.cookbook.yaml package - Update README with new recipe documentation - Add comprehensive test coverage for new recipe
1 parent a9e1814 commit 9918b78

File tree

5 files changed

+446
-12
lines changed

5 files changed

+446
-12
lines changed

README.md

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
Custom OpenRewrite recipes by Anacoders.
66

7-
## CreateYamlFilesByPattern
7+
## Recipes
8+
9+
### CreateYamlFilesByPattern
810

911
Creates YAML files in multiple directories matching a wildcard pattern. Files are only created if they don't already exist.
1012

11-
### Quick Example
13+
#### Quick Example
1214

1315
Create a `config.yaml` file in every subdirectory under `projects/`:
1416

@@ -18,7 +20,7 @@ type: specs.openrewrite.org/v1beta/recipe
1820
name: com.yourorg.CreateProjectConfigs
1921
displayName: Create config files in all projects
2022
recipeList:
21-
- com.anacoders.cookbook.CreateYamlFilesByPattern:
23+
- com.anacoders.cookbook.yaml.CreateYamlFilesByPattern:
2224
filePattern: projects/*/config.yaml
2325
fileContents: |
2426
apiVersion: v1
@@ -27,25 +29,63 @@ recipeList:
2729
name: example
2830
```
2931
30-
### Pattern Examples
32+
#### Pattern Examples
3133
3234
| Pattern | Description | Creates Files In |
3335
|---------|-------------|------------------|
3436
| `projects/*/config.yaml` | Single wildcard `*` | Each direct subdirectory of `projects/` |
3537
| `apps/*/deployment.yaml` | Works at any depth | Each subdirectory under `apps/` |
3638
| `src/main/*/application.yaml` | Nested paths | Each subdirectory under `src/main/` |
3739
| `apps/*/config/*/settings.yaml` | Multiple wildcards | Matching nested paths |
40+
| `src/**/config.yaml` | Recursive `**` | Any depth under `src/` |
3841

39-
**Note:** `**` for recursive matching is mentioned in the recipe description but not fully implemented yet.
40-
41-
### Behavior
42+
#### Behavior
4243

4344
- ✅ Creates files only in directories that match the pattern
4445
- ✅ Skips creation if the file already exists (never overwrites)
4546
- ✅ Works with any directory depth and nesting
4647
- ✅ Supports multiple `*` wildcards in a single pattern
4748

48-
## Using This Recipe
49+
### ChangeYamlPropertyConditionally
50+
51+
Updates a YAML property value only when another property in the same document matches a specified condition. Useful for updating values in multi-document YAML files where each document should be evaluated independently.
52+
53+
#### Quick Example
54+
55+
Update `replicas` to `3` only in deployments where `environment` is `production`:
56+
57+
```yaml
58+
---
59+
type: specs.openrewrite.org/v1beta/recipe
60+
name: com.yourorg.ScaleProductionDeployments
61+
displayName: Scale production deployments
62+
recipeList:
63+
- com.anacoders.cookbook.yaml.ChangeYamlPropertyConditionally:
64+
conditionJsonPath: $.metadata.labels.environment
65+
conditionValue: production
66+
targetJsonPath: $.spec.replicas
67+
newValue: "3"
68+
```
69+
70+
#### Options
71+
72+
| Option | Description | Example |
73+
|--------|-------------|---------|
74+
| `conditionJsonPath` | JsonPath to the property that must match | `$.metadata.labels.environment` |
75+
| `conditionValue` | The value that conditionJsonPath must equal | `production` |
76+
| `targetJsonPath` | JsonPath to the property to update | `$.spec.replicas` |
77+
| `newValue` | The new value to set | `3` |
78+
| `filePattern` | Optional glob to filter files | `**/k8s/**/*.yaml` |
79+
80+
#### Behavior
81+
82+
- ✅ Updates target property only when condition matches
83+
- ✅ Handles multi-document YAML files (each document evaluated independently)
84+
- ✅ No change if target value already matches
85+
- ✅ No change if condition or target property is missing
86+
- ✅ Optional file pattern filtering
87+
88+
## Using These Recipes
4989

5090
### In a Gradle Project
5191

@@ -80,13 +120,24 @@ Then run:
80120

81121
### Direct Usage
82122

83-
You can also use the recipe directly in code:
123+
You can also use the recipes directly in code:
84124

85125
```java
86-
var recipe = new CreateYamlFilesByPattern(
126+
import com.anacoders.cookbook.yaml.CreateYamlFilesByPattern;
127+
import com.anacoders.cookbook.yaml.ChangeYamlPropertyConditionally;
128+
129+
var createRecipe = new CreateYamlFilesByPattern(
87130
"projects/*/config.yaml",
88131
"apiVersion: v1\nkind: Config"
89132
);
133+
134+
var changeRecipe = new ChangeYamlPropertyConditionally(
135+
"$.metadata.labels.environment",
136+
"production",
137+
"$.spec.replicas",
138+
"3",
139+
null
140+
);
90141
```
91142

92143
## Development
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.anacoders.cookbook.yaml;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.jspecify.annotations.NullMarked;
21+
import org.jspecify.annotations.Nullable;
22+
import org.openrewrite.*;
23+
import org.openrewrite.yaml.JsonPathMatcher;
24+
import org.openrewrite.yaml.YamlIsoVisitor;
25+
import org.openrewrite.yaml.tree.Yaml;
26+
27+
import java.util.concurrent.atomic.AtomicBoolean;
28+
29+
/**
30+
* Updates a YAML property value only when another property in the same document matches a
31+
condition.
32+
* Handles multi-document YAML files correctly, evaluating each document independently.
33+
*/
34+
@NullMarked
35+
@Value
36+
@EqualsAndHashCode(callSuper = false)
37+
public class ChangeYamlPropertyConditionally extends Recipe {
38+
39+
@Option(displayName = "Condition JsonPath",
40+
description = "JsonPath to the property that must match for the update to occur.",
41+
example = "$.metadata.labels.environment")
42+
String conditionJsonPath;
43+
44+
@Option(displayName = "Condition value",
45+
description = "The value that conditionJsonPath must equal for the update to occur.",
46+
example = "production")
47+
String conditionValue;
48+
49+
@Option(displayName = "Target JsonPath",
50+
description = "JsonPath to the property to update.",
51+
example = "$.spec.replicas")
52+
String targetJsonPath;
53+
54+
@Option(displayName = "New value",
55+
description = "The new value to set at targetJsonPath.",
56+
example = "3")
57+
String newValue;
58+
59+
@Option(displayName = "File pattern",
60+
description = "A glob expression for files to process. Blank/null matches all.",
61+
required = false,
62+
example = "**/k8s/**/*.yaml")
63+
@Nullable
64+
String filePattern;
65+
66+
@Override
67+
public String getDisplayName() {
68+
return "Change YAML property conditionally";
69+
}
70+
71+
@Override
72+
public String getDescription() {
73+
return "Updates a YAML property value only when another property in the same document " +
74+
"matches a specified condition. " +
75+
"Useful for updating values in multi-document YAML files where each document " +
76+
"should be evaluated independently.";
77+
}
78+
79+
@Override
80+
public String getInstanceNameSuffix() {
81+
return String.format("`%s` to `%s` when `%s` = `%s`", targetJsonPath, newValue,
82+
conditionJsonPath, conditionValue);
83+
}
84+
85+
@Override
86+
public TreeVisitor<?, ExecutionContext> getVisitor() {
87+
JsonPathMatcher conditionMatcher = new JsonPathMatcher(conditionJsonPath);
88+
JsonPathMatcher targetMatcher = new JsonPathMatcher(targetJsonPath);
89+
90+
return Preconditions.check(new FindSourceFiles(filePattern), new
91+
YamlIsoVisitor<ExecutionContext>() {
92+
93+
@Override
94+
public Yaml.Document visitDocument(Yaml.Document document, ExecutionContext ctx) {
95+
// Check if this document meets the condition
96+
AtomicBoolean conditionMet = new AtomicBoolean(false);
97+
98+
new YamlIsoVisitor<ExecutionContext>() {
99+
@Override
100+
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry,
101+
ExecutionContext ctx) {
102+
if (conditionMatcher.matches(getCursor())) {
103+
if (entry.getValue() instanceof Yaml.Scalar) {
104+
String value = ((Yaml.Scalar) entry.getValue()).getValue();
105+
if (conditionValue.equals(value)) {
106+
conditionMet.set(true);
107+
}
108+
}
109+
}
110+
return super.visitMappingEntry(entry, ctx);
111+
}
112+
}.visit(document, ctx);
113+
114+
// If condition is met, update the target property
115+
if (conditionMet.get()) {
116+
Yaml.Document updated = (Yaml.Document) new YamlIsoVisitor<ExecutionContext>() {
117+
@Override
118+
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry,
119+
ExecutionContext ctx) {
120+
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
121+
if (targetMatcher.matches(getCursor())) {
122+
if (e.getValue() instanceof Yaml.Scalar) {
123+
Yaml.Scalar scalar = (Yaml.Scalar) e.getValue();
124+
if (!newValue.equals(scalar.getValue())) {
125+
e = e.withValue(scalar.withValue(newValue));
126+
}
127+
}
128+
}
129+
return e;
130+
}
131+
}.visit(document, ctx);
132+
return updated != null ? updated : document;
133+
}
134+
135+
return document;
136+
}
137+
});
138+
}
139+
}

src/main/java/com/anacoders/cookbook/CreateYamlFilesByPattern.java renamed to src/main/java/com/anacoders/cookbook/yaml/CreateYamlFilesByPattern.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.anacoders.cookbook;
16+
package com.anacoders.cookbook.yaml;
1717

1818
import lombok.EqualsAndHashCode;
1919
import lombok.Value;

0 commit comments

Comments
 (0)